<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
  <title>Peter Steinberger</title>
  <link>https://aggr.aymericbeaumet.com/sources/steipete.me/</link>
  <description>Browse aymeric&#39;s, an independent, searchable archive of readable snapshots from followed feeds, preserved in Git with aggr.</description>
  <language>en</language>
  <atom:link href="https://aggr.aymericbeaumet.com/sources/steipete.me/rss.xml" rel="self" type="application/rss+xml"/>
  <lastBuildDate>Sun, 15 Feb 2026 00:00:00 +0000</lastBuildDate>
  <generator>aggr</generator>
  <item>
    <title>OpenClaw, OpenAI and the future</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2026-02-15-openclaw-openai-and-the-future/</link>
    <guid isPermaLink="false">urn:aggr:item:a1d52eb9c5751072d6e2974fd846fc3fe8fa410a</guid>
    <atom:link href="https://steipete.me/posts/2026/openclaw/" rel="via" type="text/html"/>
    <pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
    <description>I&#39;m joining OpenAI to work on bringing agents to everyone. OpenClaw will move to a foundation and stay open and independent.</description>
    <content:encoded>&lt;p&gt;&lt;strong&gt;tl;dr: I’m joining OpenAI to work on bringing agents to everyone. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://openclaw.ai/&quot;&gt;OpenClaw&lt;/a&gt; will move to a foundation and stay open and independent.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The last month was a whirlwind, never would I have expected that my playground project would create such waves. The internet got weird again, and it’s been incredibly fun to see how my work inspired so many people around the world.&lt;/p&gt;
&lt;p&gt;There’s an endless array of possibilities that opened up for me, countless people trying to push me into various directions, giving me advice, asking how they can invest or what I will do. Saying it’s overwhelming is an understatement.&lt;/p&gt;
&lt;p&gt;When I started exploring AI, my goal was to have fun and inspire people. And here we are, the lobster is taking over the world. My next mission is to build an agent that even my mum can use. That’ll need a much broader change, a lot more thought on how to do it safely, and access to the very latest models and research.&lt;/p&gt;
&lt;p&gt;Yes, I could totally see how OpenClaw could become a huge company. And no, it’s not really exciting for me. I’m a builder at heart. I did the whole creating-a-company game already, poured 13 years of my life into it and learned a lot. What I want is to change the world, not build a large company and teaming up with OpenAI is the fastest way to bring this to everyone.&lt;/p&gt;
&lt;p&gt;I spent last week in San Francisco talking with the major labs, getting access to people and unreleased research, and it’s been inspiring on all fronts. I want to thank all the folks I talked to this week and am thankful for the opportunities.&lt;/p&gt;
&lt;p&gt;It’s always been important to me that OpenClaw stays open source and given the freedom to flourish. Ultimately, I felt OpenAI was the best place to continue pushing on my vision and expand its reach. The more I talked with the people there, the clearer it became that we both share the same vision.&lt;/p&gt;
&lt;p&gt;The community around OpenClaw is something magical and OpenAI has made strong commitments to enable me to dedicate my time to it and already sponsors the project. To get this into a proper structure I’m working on making it a foundation. It will stay a place for thinkers, hackers and people that want a way to own their data, with the goal of supporting even more models and companies.&lt;/p&gt;
&lt;p&gt;Personally I’m super excited to join OpenAI, be part of the frontier of AI research and development, and continue building with all of you.&lt;/p&gt;
&lt;p&gt;The claw is the law.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2026/openclaw/clawcon.jpg&quot; alt=&quot;ClawCon&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Shipping at Inference-Speed</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-12-28-shipping-at-inference-speed/</link>
    <guid isPermaLink="false">urn:aggr:item:61132b46751298632792f2484b0cdf624b1fd37c</guid>
    <atom:link href="https://steipete.me/posts/2025/shipping-at-inference-speed/" rel="via" type="text/html"/>
    <pubDate>Sun, 28 Dec 2025 11:00:00 +0000</pubDate>
    <description>Why I stopped reading code and started watching it stream by.</description>
    <content:encoded>&lt;h2&gt;What Changed Since May&lt;/h2&gt;
&lt;p&gt;It’s incredible how far “vibe coding” has come this year. Whereas in ~May I was amazed that &lt;em&gt;some&lt;/em&gt; prompts produced code that worked out of the box, &lt;strong&gt;this is now my expectation&lt;/strong&gt;. I can ship code now at a speed that seems unreal. I burned &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/thsottiaux/status/2004789121492156583&quot;&gt;a lot of tokens&lt;/a&gt; since then. Time for an update.&lt;/p&gt;
&lt;p&gt;It’s funny how these agents work. There’s been this argument a few weeks ago that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1997380251081490717&quot;&gt;one needs to write code in order to feel bad architecture&lt;/a&gt; and that using agents creates a disconnection - and I &lt;strong&gt;couldn’t disagree more&lt;/strong&gt;. When you spend enough time with agents, you know exactly how long sth should take, and when codex comes back and hasn’t solved it in one shot, I already get suspicious.&lt;/p&gt;
&lt;p&gt;The amount of software I can create is now mostly &lt;strong&gt;limited by inference time and hard thinking&lt;/strong&gt;. And let’s be honest - most software does not require hard thinking. Most apps shove data from one form to another, maybe store it somewhere, and then show it to the user in some way or another. The simplest form is text, so by default, whatever I wanna build, it starts as CLI. Agents can call it directly and verify output - closing the loop.&lt;/p&gt;
&lt;h2&gt;The Model Shift&lt;/h2&gt;
&lt;p&gt;The real unlock into &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/&quot;&gt;building like a factory&lt;/a&gt; was GPT 5. It took me a few weeks after the release to see it - and for codex to catch up on features that claude code had, and a bit to learn and understand the differences, but then I started trusting the model more and more. &lt;strong&gt;These days I don’t read much code anymore.&lt;/strong&gt; I watch the stream and sometimes look at key parts, but I gotta be honest - most code I don’t read. I do know where which components are and how things are structured and how the overall system is designed, and that’s usually all that’s needed.&lt;/p&gt;
&lt;p&gt;The important decisions these days are &lt;strong&gt;language/ecosystem and dependencies&lt;/strong&gt;. My go-to languages are TypeScript for web stuff, Go for CLIs and Swift if it needs to use macOS stuff or has UI. Go wasn’t something I gave even the slightest thought even a few months ago, but eventually I played around and found that agents are really great at writing it, and its simple type system makes linting fast.&lt;/p&gt;
&lt;p&gt;Folks building Mac or iOS stuff: You don’t need Xcode much anymore. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/clawdis/tree/main/apps/ios&quot;&gt;I don’t even use xcodeproj files&lt;/a&gt;. Swift’s build infra is good enough for most things these days. codex knows how to run iOS apps and how to deal with the Simulator. No special stuff or MCPs needed.&lt;/p&gt;
&lt;h2&gt;codex vs Opus&lt;/h2&gt;
&lt;p&gt;I’m writing this post here while codex crunches through a huge, multi-hour refactor and un-slops older crimes of Opus 4.0. People on Twitter often ask me what’s the big difference between Opus and codex and why it even matters because the benchmarks are so close. IMO it’s getting harder and harder to trust benchmarks - you need to try both to really understand. Whatever OpenAI did in post-training, codex has been trained to read LOTS of code before starting.&lt;/p&gt;
&lt;p&gt;Sometimes it just &lt;strong&gt;silently reads files for 10, 15 minutes&lt;/strong&gt; before starting to write any code. On the one hand that’s annoying, on the other hand that’s amazing because it greatly increases the chance that it fixes the right thing. Opus on the other hand is much more eager - great for smaller edits - not so good for larger features or refactors, it often doesn’t read the whole file or misses parts and then delivers inefficient outcomes or misses sth. I noticed that even tho codex sometimes takes 4x longer than Opus for comparable tasks, I’m often faster because I don’t have to go back and fix the fix, sth that felt quite normal when I was still using Claude Code.&lt;/p&gt;
&lt;p&gt;codex also allowed me to unlearn lots of charades that were necessary with Claude Code. Instead of “&lt;strong&gt;plan mode&lt;/strong&gt;”, I simply &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1997412175615246603&quot;&gt;&lt;strong&gt;start a conversation with the model&lt;/strong&gt;&lt;/a&gt;, ask a question, let it google, explore code, create a plan together, and when I’m happy with what I see, I write “build” or “write plan to docs/*.md and build this”. Plan mode feels like a hack that was necessary for older generations of models that were not great at adhering to prompts, so we had to take away their edit tools. There’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/2001228002953158928&quot;&gt;a highly misunderstood tweet of mine&lt;/a&gt; that’s still circling around that showed me that most people don’t get that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://lucumr.pocoo.org/2025/12/17/what-is-plan-mode/&quot;&gt;plan mode is not magic&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Oracle&lt;/h2&gt;
&lt;p&gt;The step from GPT 5/5.1 to 5.2 was massive. I built &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/oracle&quot;&gt;&lt;strong&gt;oracle 🧿&lt;/strong&gt;&lt;/a&gt; about a month ago - it’s a CLI that allows the agent to run GPT 5 Pro and upload files + a prompt and manages sessions so answers can be retrieved later. I did this because many times when agents were stuck, I asked it to write everything into a markdown file and then did the query myself, and that felt like a repetitive waste of time - and an opportunity to close the loop. The instructions are in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/agent-scripts/blob/main/AGENTS.MD&quot;&gt;my global AGENTS.MD&lt;/a&gt; file and the model sometimes by itself triggered oracle when it got stuck. I used this multiple times per day. It was a &lt;strong&gt;massive unlock&lt;/strong&gt;. Pro is insanely good at doing a speedrun across ~50 websites and then thinking really hard at it and in almost every case nailed the response. Sometimes it’s fast and takes 10 minutes, but I had runs that took more than an hour.&lt;/p&gt;
&lt;p&gt;Now that GPT 5.2 is out, I have far fewer situations where I need it. I do use Pro myself sometimes for research, but the cases where I asked the model to “ask the oracle” went from multiple times per day to a few times per week. I’m not mad about this - building oracle was super fun and I learned lots about browser automation, Windows and finally took my time to look into skills, after dismissing that idea for quite some time. What it does show is how much better 5.2 got for many real-life coding tasks. It &lt;strong&gt;one-shots almost anything&lt;/strong&gt; I throw at it.&lt;/p&gt;
&lt;p&gt;Another massive win is the &lt;strong&gt;knowledge cutoff date&lt;/strong&gt;. GPT 5.2 goes till end of August whereas Opus is stuck in mid-March - that’s about 5 months. Which is significant when you wanna use the latest available tools.&lt;/p&gt;
&lt;h2&gt;A Concrete Example: VibeTunnel&lt;/h2&gt;
&lt;p&gt;To give you another example on how far models have come. One of my early intense projects was &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibetunnel.sh/&quot;&gt;VibeTunnel&lt;/a&gt;. A terminal-multiplexer so you can code on-the-go. I poured pretty much all my time into this earlier this year, and after 2 months it was so good that I caught myself coding from my phone while out with friends… and decided that this is something I should stop, more for mental health than anything. Back then I tried to rewrite a core part of the multiplexer away from TypeScript, and the older models consistently failed me. I tried Rust, Go… god forbid, even zig. Of course I could have finished this refactor, but it would have required lots of manual work, so I never got around completing this before I put it to rest. Last week I un-dusted this and gave codex a &lt;strong&gt;two sentence prompt&lt;/strong&gt; to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/compare/6a1693b482fa4ef0ac021700a9ec05489a3a108f...a81b29ee3de6a2c85fd9fa41423d968dcc000515&quot;&gt;convert the whole forwarding-system to zig&lt;/a&gt;, and it ran over 5h and multiple compactions and delivered a working conversion in one shot.&lt;/p&gt;
&lt;p&gt;Why did I even un-dust it, you ask? My current focus is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://clawdis.ai/&quot;&gt;Clawdis&lt;/a&gt;, an AI assistant that has &lt;strong&gt;full access to everything&lt;/strong&gt; on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/2005213014778409280/photo/1&quot;&gt;all my computers&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://imsg.to/&quot;&gt;messages&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/gogcli&quot;&gt;emails&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.openhue.io/cli/openhue-cli&quot;&gt;home automation&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://camsnap.ai/&quot;&gt;cameras&lt;/a&gt;, lights, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sonoscli.sh/&quot;&gt;music&lt;/a&gt;, heck it can even control the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://eightctl.sh/&quot;&gt;temperature of my bed&lt;/a&gt;. Ofc it also has &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/sag/&quot;&gt;its own voice&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/bird&quot;&gt;a CLI to tweet&lt;/a&gt; and its own &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://clawd.bot&quot;&gt;clawd.bot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Clawd &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.peekaboo.boo/&quot;&gt;can see and control my screen&lt;/a&gt; and sometimes makes snarky remarks, but I also wanted to give him the ability to check on my agents, and getting a &lt;strong&gt;character stream&lt;/strong&gt; is just far more efficient than looking at images… if this will work out, we’ll see!&lt;/p&gt;
&lt;h2&gt;My Workflow&lt;/h2&gt;
&lt;p&gt;I know… you came here to &lt;strong&gt;learn how to build faster&lt;/strong&gt;, and I’m just writing a marketing-pitch for OpenAI. I hope Anthropic is cooking Opus 5 and the tides turn again. Competition is good! At the same time, I &lt;em&gt;love&lt;/em&gt; Opus as general purpose model. My AI agent wouldn’t be half as fun running on GPT 5. Opus has something &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://soul.md/&quot;&gt;special&lt;/a&gt; that makes it a delight to work with. I use it for most of my computer automation tasks and ofc it powers Clawd🦞.&lt;/p&gt;
&lt;p&gt;I haven’t changed my workflow all that much from &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/just-talk-to-it&quot;&gt;my last take at it in October&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;I usually work on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/2005083410482733427/photo/1&quot;&gt;&lt;strong&gt;multiple projects&lt;/strong&gt;&lt;/a&gt; at the same time. Depending on complexity that can be between 3-8. The context switching can be tiresome, I really only can do that when I’m working at home, in silence and concentrated. It’s a lot of mental models to shuffle. Luckily most software is boring. Creating a CLI to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ordercli.sh/&quot;&gt;check up on your food delivery&lt;/a&gt; doesn’t need a lot of thinking. Usually my focus is on one big project and satellite projects that chug along. When you do enough agentic engineering, you develop a feeling for what’s gonna be easy and where the model likely will struggle, so often I just put in a prompt, codex will chug along for 30 minutes and I have what I need. Sometimes it takes a little fiddling or creativity, but often things are straightforward.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I extensively use the &lt;strong&gt;queueing feature&lt;/strong&gt; of codex - as I get a new idea, I add it to the pipeline. I see many folks experimenting with various systems of multi-agent orchestration, emails or automatic task management - so far I don’t see much need for this - usually I’m the bottleneck. My approach to building software is very iterative. I build sth, play with it, see how it “feels”, and then get new ideas to refine it. Rarely do I have a complete picture of what I want in my head. Sure, I have a rough idea, but often that drastically changes as I explore the problem domain. So systems that take &lt;em&gt;the complete idea&lt;/em&gt; as input and then deliver output wouldn’t work well for me. I need to play with it, touch it, feel it, see it, that’s how I evolve it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I basically &lt;strong&gt;never revert&lt;/strong&gt; or use checkpointing. If something isn’t how I like it, I ask the model to change it. codex sometimes then resets a file, but often it simply reverts or modifies the edits, very rare that I have to back completely, and instead we just travel into a different direction. Building software is like walking up a mountain. You don’t go straight up, you circle around it and take turns, sometimes you get off path and have to walk a bit back, and it’s imperfect, but eventually you get to where you need to be.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I simply &lt;strong&gt;commit to main&lt;/strong&gt;. Sometimes codex decides that it’s too messy and automatically creates a worktree and then merges changes back, but it’s rare and I only prompt that in exceptional cases. I find the added cognitive load of having to think of different states in my projects unnecessary and prefer to evolve it linearly. Bigger tasks I keep for moments where I’m distracted - for example while writing this, I run refactors on 4 projects here that will take around 1-2h each to complete. Ofc I could do that in a worktree, but that would just cause lots of merge conflicts and suboptimal refactors. Caveat: I usually work alone, if you work in a bigger team that workflow obv won’t fly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I’ve already mentioned my way of planning a feature. I &lt;strong&gt;cross-reference projects&lt;/strong&gt; all the time, esp if I know that I already solved sth somewhere else, I ask codex to look in ../project-folder and that’s usually enough for it to infer from context where to look. This is extremely useful to save on prompts. I can just write “look at ../vibetunnel and do the same for Sparkle changelogs”, because it’s already solved there and with a 99% guarantee it’ll correctly copy things over and adapt to the new project. That’s how I scaffold new projects as well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I’ve seen plenty of systems for folks wanting to refer to past sessions. Another thing I never need or use. I maintain docs for subsystems and features in a &lt;strong&gt;docs folder&lt;/strong&gt; in each project, and use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/agent-scripts/blob/main/scripts/docs-list.ts&quot;&gt;a script + some instructions&lt;/a&gt; in my global AGENTS file to force the model to read docs on certain topics. This pays off more the larger the project is, so I don’t use it everywhere, but it is of great help to keep docs up-to-date and engineer a better context for my tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apropos context. I used to be really diligent to restart a session for new tasks. &lt;strong&gt;With GPT 5.2&lt;/strong&gt; this is no longer needed. Performance is extremely good even when the context is fuller, and often it helps with speed since the model works faster when it already has loaded plenty files. Obviously that only works well when you serialize your tasks or keep the changes so far apart that two sessions don’t touch each other much. codex has no system events for “this file changed”, unlike claude code, so you need to be more careful - on the flip side, codex is just FAR better at context management, I feel I get 5x more done on one codex session than with claude. This is more than just the objectively larger context size, there’s other things at work. My guess is that codex internally thinks really condensed to save tokens, whereas Opus is very wordy. Sometimes the model messes up and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1974108054984798729&quot;&gt;its internal thinking stream leaks to the user&lt;/a&gt;, so I’ve seen this quite a few times. Really, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/2005243588414931368&quot;&gt;codex has a way with words&lt;/a&gt; I find strangely entertaining.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Prompts. I used to write long, elaborate prompts with voice dictation. With codex, my &lt;strong&gt;prompts gotten much shorter&lt;/strong&gt;, I often type again, and many times I add images, especially when iterating on UI (or text copies with CLIs). If you show the model what’s wrong, just a few words are enough to make it do what you want. Yes, I’m that person that drags in a clipped image of some UI component with “fix padding” or “redesign”, many times that either solves my issue or gets me reasonably far. I used to refer to markdown files, but with my docs:list script that’s no longer necessary.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Markdowns. Many times I write “&lt;strong&gt;write docs to docs/*.md&lt;/strong&gt;” and simply let the model pick a filename. The more obvious you design the structure for what the model is trained on, the easier your work will be. After all, I don’t design codebases to be easy to navigate for me, I engineer them so agents can work in it efficiently. Fighting the model is often a waste of time and tokens.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tooling &amp;amp; Infrastructure&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What’s still hard?&lt;/strong&gt; Picking the right dependency and framework to set on is something I invest quite some time on. Is this well-maintained? How about peer dependencies? Is it popular = will have enough world knowledge so agents have an easy time? Equally, system design. Will we communicate via web sockets? HTML? What do I put into the server and what into the client? How and which data flows where to where? Often these are things that are a bit harder to explain to a model and where research and thinking pays off.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Since I manage lots of projects, often I let an agent simply run in my project folder and when I figure out a new pattern, I ask it to “&lt;strong&gt;find all my recent go projects&lt;/strong&gt; and implement this change there too + update changelog”. Each of my project has a raised patch version in that file and when I revisit it, some improvements are already waiting for me to test.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ofc I &lt;strong&gt;automate everything&lt;/strong&gt;. There’s a skill to register domains and change DNS. One to write good frontends. There’s a note in my AGENTS file about my tailscale network so I can just say “go to my mac studio and update xxx”.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apropos &lt;strong&gt;multiple Macs&lt;/strong&gt;. I usually work on two Macs. My MacBook Pro on the big screen, and a Jump Desktop session to my Mac Studio on another screen. Some projects are cooking there, some here. Sometimes I edit different parts of the same project on each machine and sync via git. Simpler than worktrees because drifts on main are easy to reconcile. Has the added benefit that anything that needs UI or browser automation I can move to my Studio and it won’t annoy me with popups. (Yes, Playwright has headless mode but there’s enough situations where that won’t work)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Another benefit is that tasks &lt;strong&gt;keep running&lt;/strong&gt; there, so whenever I travel, remote becomes my main workstation and tasks simply keep running even if I close my Mac. I did experiment with real async agents like codex or Cursor web in the past, but I miss the steerability, and ultimately the work ends up as pull request, which again adds complexity to my setup. I much prefer the simplicity of the terminal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I used to play with slash commands, but just never found them too useful. Skills replaced some of it, and for the rest I keep writing “&lt;strong&gt;commit/push&lt;/strong&gt;” because it takes the same time as /commit and always works.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the past I often took dedicated days to &lt;strong&gt;refactor and clean up&lt;/strong&gt; projects, I do this much more ad-hoc now. Whenever prompts start taking too long or I see sth ugly flying by in the code stream, I’ll deal with it right away.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I tried linear or other &lt;strong&gt;issue trackers&lt;/strong&gt;, but nothing did stick. Important ideas I try right away, and everything else I’ll either remember or it wasn’t important. Of course I have public bug trackers for bugs for folks that use my open source code, but when I find a bug, I’ll immediately prompt it - much faster than writing it down and then later having to switch context back to it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Whatever you build, &lt;strong&gt;start with the model and a CLI first&lt;/strong&gt;. I had this idea of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/2005320848543298009&quot;&gt;a Chrome extension to summarize YouTube&lt;/a&gt; vids in my head for a long time. Last week I started working on summarize, a CLI that converts anything to markdown and then feeds that to a model for summarization. First I got the core right, and once that worked great I built the whole extension in a day. I’m quite in love with it. Runs on local, free or paid models. Transcribes video or audio locally. Talks to a local daemon so it’s super fast. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/summarize/releases/latest&quot;&gt;Give it a go!&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;My go-to model is &lt;strong&gt;gpt-5.2-codex high&lt;/strong&gt;. Again, KISS. There’s very little benefit to xhigh other than it being far slower, and I don’t wanna spend time thinking about different modes or “ultrathink”. So pretty much everything runs on high. GPT 5.2 and codex are close enough that changing models makes no sense, so I just use that.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;My Config&lt;/h2&gt;
&lt;p&gt;This is my &lt;code&gt;~/.codex/config.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;model = &amp;quot;gpt-5.2-codex&amp;quot;
model_reasoning_effort = &amp;quot;high&amp;quot;
tool_output_token_limit = 25000
# Leave room for native compaction near the 272–273k context window.
# Formula: 273000 - (tool_output_token_limit + 15000)
# With tool_output_token_limit=25000 ⇒ 273000 - (25000 + 15000) = 233000
model_auto_compact_token_limit = 233000
[features]
ghost_commit = false
unified_exec = true
apply_patch_freeform = true
web_search_request = true
skills = true
shell_snapshot = true

[projects.&amp;quot;/Users/steipete/Projects&amp;quot;]
trust_level = &amp;quot;trusted&amp;quot;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows the model to read more in one go, the defaults are a bit small and can limit what it sees. It fails silently, which is a pain and something they’ll eventually fix. Also, web search is still not on by default? &lt;code&gt;unified_exec&lt;/code&gt; replaced tmux and my old &lt;code&gt;runner&lt;/code&gt; script, rest’s neat too. And don’t be scared about compaction, ever since OpenAI switched to their new /compact endpoint, this works well enough that tasks can run across many compacts and will be finished. It’ll make things slower, but often acts like a review, and the model will find bugs when it looks at code again.&lt;/p&gt;
&lt;p&gt;That’s it, for now. I plan on writing more again and have quite a backlog on ideas in my head, just having &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://codexbar.app/&quot;&gt;&lt;strong&gt;too much fun&lt;/strong&gt;&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/2005393881395835045&quot;&gt;&lt;strong&gt;building things&lt;/strong&gt;&lt;/a&gt;. If you wanna hear more ramblings and ideas how to build in this new world, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete&quot;&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>The Signature Flicker</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-12-18-the-signature-flicker/</link>
    <guid isPermaLink="false">urn:aggr:item:ca212a0cd1814e553bd8c038a354da6c128e57d2</guid>
    <atom:link href="https://steipete.me/posts/2025/signature-flicker/" rel="via" type="text/html"/>
    <pubDate>Thu, 18 Dec 2025 00:00:00 +0000</pubDate>
    <description>Hell froze over. Anthropic fixed Claude Code&#39;s signature flicker in their latest update (2.0.72)</description>
    <content:encoded>&lt;p&gt;&lt;strong&gt;tl;dr: Hell froze over. Anthropic fixed Claude Code’s signature flicker in their latest update (2.0.72)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/signature-flicker/claude-flicker.gif&quot; alt=&quot;Claude Code flickering&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;If there’s one thing everybody noticed about Claude Code (apart from it revolutionizing how we build software), &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.reddit.com/r/ClaudeAI/comments/1lxs53r/what_is_this_madness/&quot;&gt;it’s the flickering&lt;/a&gt;. It’s unfair to point to Anthropic since they are not alone: other TUIs such as Cursor, or really anything based on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/vadimdemedes/ink&quot;&gt;Ink&lt;/a&gt; have the same issue. It’s also not an easy problem at all. Claude Code uses React under the hood.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fnref-1&quot; data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;The Issue&lt;/h2&gt;
&lt;p&gt;Terminals haven’t really been designed for interactivity. It’s possible to use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://en.wikipedia.org/wiki/ANSI_escape_code&quot;&gt;ANSI escape codes&lt;/a&gt; to reposition the cursor and write over existing text, but that easily leads to flickering if not done well.&lt;/p&gt;
&lt;p&gt;There are two ways to solve this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Switch to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ratatui.rs/concepts/backends/alternate-screen/#:~:text=The%20alternate%20screen%20is%20a,content%20of%20the%20main%20screen.&quot;&gt;alt mode&lt;/a&gt; and take complete control over the terminal viewport.&lt;/li&gt;
&lt;li&gt;Carefully re-render changed parts while leaving the scrollback unchanged.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Neither option is great, and each comes with tradeoffs. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://mariozechner.at/posts/2025-11-30-pi-coding-agent/#toc_6&quot;&gt;Mario Zechner explains these really well&lt;/a&gt; in his write-up on building pi-coding-agent, so I won’t repeat the details here. I’ve spent time with option 2 myself, having ported &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/badlogic/pi-mono/tree/main/packages/tui&quot;&gt;pi-tui&lt;/a&gt; to Swift as &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/TauTUI&quot;&gt;TauTUI&lt;/a&gt; (Codex did most of the auto-translation).&lt;/p&gt;
&lt;p&gt;For a coding agent that mostly emits text with limited interactivity, I believe carefully re-rendering only changed parts — while remaining a good terminal citizen — is the better approach.&lt;/p&gt;
&lt;p&gt;Anthropic seems to agree. Ink, the React-based terminal renderer Claude Code originally used, didn’t support the kind of fine-grained incremental updates needed for a long-running interactive UI. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/vadimdemedes/ink/pull/781&quot;&gt;This has since improved upstream&lt;/a&gt;, but Anthropic needed tighter control, so they &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/anthropics/claude-code/issues/769#issuecomment-3667315590&quot;&gt;rewrote the renderer from scratch&lt;/a&gt; — while still keeping React as the component model.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is kind of like if a website were to do their own text rendering, highlighting, mouse movement, context menu, etc. — it would not feel like your browser. (…) We value this native experience a lot. We may explore alternate screen mode in the future, but our bar is quite high. — &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/trq212/status/2001552877698056370&quot;&gt;Thariq&lt;/a&gt;, Anthropic&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Landscape&lt;/h2&gt;
&lt;p&gt;Over the last year, most new coding agents have converged on alt-screen TUIs — often after fighting flicker — but the results haven’t been great. So what’s my big gripe with alt mode? It breaks features such as text selection, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/mitchellh/status/1978934533170041118&quot;&gt;native scrolling&lt;/a&gt; or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/mitchellh/status/1993728538344906978&quot;&gt;search&lt;/a&gt;. Of course this can be implemented in the TUI, but it won’t feel &lt;em&gt;right&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;Amp&lt;/h3&gt;
&lt;p&gt;Amp used Ink and shared Claude’s flickering but &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ampcode.com/news/look-ma-no-flicker&quot;&gt;eventually wrote their own renderer, switching to alt mode in September&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/signature-flicker/amp-find.gif&quot; alt=&quot;amp find demo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find&lt;/code&gt; fails unless the text is currently on-screen&lt;/li&gt;
&lt;li&gt;No native-feeling selection/context menu flow&lt;/li&gt;
&lt;li&gt;Custom scrollbar; workable but not quite the terminal&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Gemini&lt;/h3&gt;
&lt;p&gt;Google &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developers.googleblog.com/en/making-the-terminal-beautiful-one-pixel-at-a-time/&quot;&gt;did a big announcement on their blog&lt;/a&gt; about their new alt-mode TUI, only to learn that users hate it and then &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/google-gemini/gemini-cli/discussions/13633&quot;&gt;rolled it back not even a week later&lt;/a&gt;. In the new TUI, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/google-gemini/gemini-cli/discussions/13067&quot;&gt;you have to press CTRL-S to enter selection mode to copy text&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/signature-flicker/gemini-textselect.gif&quot; alt=&quot;gemini text selection demo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h3&gt;OpenCode&lt;/h3&gt;
&lt;p&gt;OpenCode did some great engineering and built &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/sst/opentui&quot;&gt;opentui&lt;/a&gt; in TypeScript and Zig, which renders SolidJS or React. It’s not without downsides, e.g. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/sst/opencode/issues/4043#issuecomment-3519627447&quot;&gt;it doesn’t work in the standard macOS Terminal&lt;/a&gt; for anything below macOS 26 or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/sst/opencode/issues/4320&quot;&gt;GNOME’s Terminal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/signature-flicker/opencode-text.gif&quot; alt=&quot;opencode text demo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auto-scroll near the viewport border makes small screens painful&lt;/li&gt;
&lt;li&gt;No scrollbar; search doesn’t work the way you expect&lt;/li&gt;
&lt;li&gt;Right click → paste into the input box doesn’t work&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Codex&lt;/h3&gt;
&lt;p&gt;Compare to OpenAI’s Codex, which stays in the primary screen buffer and lets me interact with text just like I expect:&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/signature-flicker/codex-real.gif&quot; alt=&quot;codex real demo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Codex isn’t perfect — it sometimes overwrites lines of text — but it already nails the part that matters most: it behaves like a terminal. That makes &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/openai/codex/blob/main/codex-rs/tui2/docs/tui_viewport_and_history.md&quot;&gt;their current direction toward an alt-mode TUI&lt;/a&gt; feel like a regression, not an upgrade. Here’s hoping they reverse course.&lt;/p&gt;
&lt;h3&gt;pi&lt;/h3&gt;
&lt;p&gt;Mario Zechner’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://shittycodingagent.ai/&quot;&gt;pi&lt;/a&gt; is currently the gold-standard for differential rendering, while also using all tricks of modern terminals — including showing inline images:&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/signature-flicker/pi-demo.gif&quot; alt=&quot;pi demo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2&gt;Verdict&lt;/h2&gt;
&lt;p&gt;Claude Code and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://shittycodingagent.ai/&quot;&gt;pi&lt;/a&gt; prove you can kill flicker without giving up the terminal’s muscle memory.&lt;/p&gt;
&lt;p&gt;Alt mode can be great for dashboards. For coding agents, though, I want the terminal’s built-ins to stay &lt;em&gt;native&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Select text like it’s a terminal&lt;/li&gt;
&lt;li&gt;Scrollback like it’s a terminal&lt;/li&gt;
&lt;li&gt;Search like it’s a terminal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s 2025. We can have smooth rendering &lt;em&gt;and&lt;/em&gt; keep the terminal’s superpowers.&lt;/p&gt;
&lt;section class=&quot;footnotes&quot; data-footnotes&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;Yes, I made the same weird look when I first learned about this, but it’s kinda beautiful. React’s concepts are flexible enough that it doesn’t require a browser as a frontend — and Ink is such an alternative render backend. &lt;a href=&quot;#fnref-1&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;1&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Just Talk To It - the no-bs Way of Agentic Engineering</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-10-14-just-talk-to-it-the-no-bs-way-of-agentic-engineering/</link>
    <guid isPermaLink="false">urn:aggr:item:12ae3f73dccc0ce1b0511ee0a346595d17275e45</guid>
    <atom:link href="https://steipete.me/posts/just-talk-to-it/" rel="via" type="text/html"/>
    <pubDate>Tue, 14 Oct 2025 09:00:00 +0000</pubDate>
    <description>A practical guide to working with AI coding agents without the hype.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/just-talk-to-it/curve-angentic.jpg&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;I’ve been more quiet here lately as I’m knee-deep working on my latest project. Agentic engineering has become so good that it now writes pretty much 100% of my code. And yet I see so many folks trying to solve issues and generating these elaborated charades instead of getting sh*t done.&lt;/p&gt;
&lt;p&gt;This post partly is inspired by the conversations I had at last night’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/christianklotz/status/1977866496001867925&quot;&gt;Claude Code Anonymous in London&lt;/a&gt; and partly since &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/pmddomingos/status/1976399060052607469&quot;&gt;it’s been an AI year&lt;/a&gt; since my last workflow update. Time for a check-in.&lt;/p&gt;
&lt;p&gt;All of the basic ideas still apply, so I won’t mention simple things like context management again. Read my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/optimal-ai-development-workflow&quot;&gt;Optimal AI Workflow post&lt;/a&gt; for a primer.&lt;/p&gt;
&lt;h2&gt;Context &amp;amp; Tech-Stack&lt;/h2&gt;
&lt;p&gt;I work by myself, current project is a ~300k LOC TypeScript React app, a Chrome extension, a cli, a client app in Tauri and a mobile app in Expo. I host on vercel, a PR delivers a new version of my website in ~2 minutes to test. Everything else (apps etc) is not automated.&lt;/p&gt;
&lt;h2&gt;Harness &amp;amp; General Approach&lt;/h2&gt;
&lt;p&gt;I’ve completely moved to &lt;code&gt;codex&lt;/code&gt; cli as daily driver. I run between 3-8 in parallel in a 3x3 terminal grid, most of them &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977771686176174352&quot;&gt;in the same folder&lt;/a&gt;, some experiments go in separate folders. I experimented with worktrees, PRs but always revert back to this setup as it gets stuff done the fastest.&lt;/p&gt;
&lt;p&gt;My agents do git &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977498385172050258&quot;&gt;atomic commits&lt;/a&gt; themselves. In order to maintain a mostly clean commit history, I iterated a lot on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/d3b9db3fa8eb1d1a692b7656217d8655&quot;&gt;my agent file&lt;/a&gt;. This makes git ops sharper so each agent commits exactly the files it edited.&lt;/p&gt;
&lt;p&gt;Yes, with claude you could do hooks and codex doesn’t support them yet, but models are incredibly clever and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977119589860601950&quot;&gt;no hook will stop them&lt;/a&gt; if they are determined.&lt;/p&gt;
&lt;p&gt;I was being ridiculed in the past and called a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/weberwongwong/status/1975749583079694398&quot;&gt;slop-generator&lt;/a&gt;, good to see that running parallel agents &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1976353767705457005&quot;&gt;slowly gets mainstream&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Model Picker&lt;/h2&gt;
&lt;p&gt;I build pretty much everything with gpt-5-codex on mid settings. It’s a great compromise of smart &amp;amp; speed, and dials thinking up/down automatically. I found over-thinking these settings to not yield meaningful results, and it’s nice not having to think about &lt;em&gt;ultrathink&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;Blast Radius 💥&lt;/h3&gt;
&lt;p&gt;Whenever I work, I think about the “blast radius”. I didn’t come up with that term, I do love it tho. When I think of a change I have a pretty good feeling about how long it’ll take and how many files it will touch. I can throw many small bombs at my codebase or a one “Fat Man” and a few small ones. If you throw multiple large bombs, it’ll be impossible to do isolated commits, much harder to reset if sth goes wrong.&lt;/p&gt;
&lt;p&gt;This is also a good indicator while I watch my agents. If something takes longer than I anticipated, I just hit escape and ask “what’s the status” to get a status update and then either help the model to find the right direction, abort or continue. Don’t be afraid of stopping models mid-way, file changes are atomic and they are really good at picking up where they stopped.&lt;/p&gt;
&lt;p&gt;When I am unsure about the impact, I use “give me a few options before making changes” to gauge it.&lt;/p&gt;
&lt;h3&gt;Why not worktrees?&lt;/h3&gt;
&lt;p&gt;I run one dev server, as I evolve my project I click through it and test multiple changes at once. Having a tree/branch per change would make this significantly slower, spawning multiple dev servers would quickly get annoying. I also have limitations for Twitter OAuth, so I can only register some domains for callbacks.&lt;/p&gt;
&lt;h3&gt;What about Claude Code?&lt;/h3&gt;
&lt;p&gt;I used to love Claude Code, these days I can’t stand it anymore (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977072732136521836&quot;&gt;even tho codex is a fan&lt;/a&gt;). It’s language, the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/vtahowe/status/1976709116425871772&quot;&gt;absolutely right’s&lt;/a&gt;, the 100% production ready messages while tests fail - I just can’t anymore. Codex is more like the introverted engineer that chugs along and just gets stuff done. It reads much more files before starting work so even small prompts usually do exactly what I want.&lt;/p&gt;
&lt;p&gt;There’s broad consensus in my timeline that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/s_streichsbier/status/1974334735829905648&quot;&gt;codex is the way&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/kimmonismus/status/1976404152541680038&quot;&gt;to go&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Other benefits of codex&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~230k usable context vs claude’s 156k.&lt;/strong&gt; Yes, there’s Sonnet 1Mio if you get lucky or pay API pricing, but realistically Claude gets very silly long before it depletes that context so it’s not realistically something you can use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More efficient token use.&lt;/strong&gt; Idk what OpenAI does different, but my context fills up far slower than with Claude Code. I used to see Compacting… all the time when using claude, I very rarely manage to exceed the context in codex.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message Queuing.&lt;/strong&gt; Codex allows to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1978099041884897517&quot;&gt;queue messages&lt;/a&gt;. Claude had this feature, but a few months ago they changed it so your messages “steer” the model. If I want to steer codex, I just press escape and enter to send the new message. Having the option for both is just far better. I often queue related feature tasks and it just reliably works them off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speed&lt;/strong&gt; OpenAI rewrote codex in Rust, and it shows. It’s incredibly fast. With Claude Code I often have multi-second freezes and it’s process blows up to gigabytes of memory. And then there’s the terminal flickering, especially when using Ghostty. Codex has none of that. It feels incredibly lightweight and fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Language.&lt;/strong&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1975297275242160395&quot;&gt;This really makes a difference to my mental health.&lt;/a&gt; I’ve been screaming at claude so many times. I rarely get angry with codex. Even if codex would be a worse model I’d use it for that fact alone. If you use both for a few weeks you will understand.&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977466373363437914&quot;&gt;No random markdown files everywhere&lt;/a&gt;. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/deepfates/status/1975604489634914326&quot;&gt;IYKYK&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why not $harness&lt;/h3&gt;
&lt;p&gt;IMO there’s simply not much space between the end user and the model company. I get by far the best deal using a subscription. I currently have 4 OpenAI subs and 1 Anthropic sub, so my overall costs are around 1k/month for basically unlimited tokens. If I’d use API calls, that’d cost my around 10x more. Don’t nail me on this math, I used some token counting tools like ccusage and it’s all somewhat imprecise, but even if it’s just 5x it’s a damn good deal.&lt;/p&gt;
&lt;p&gt;I like that we have tools like amp or Factory, I just don’t see them surviving long-term. Both codex and claude code are getting better with every release, and they all converge to the same ideas and feature set. Some might have a temporary edge with better todo lists, steering or slight dx features, but I don’t see them significantly out-competing the big AI companies.&lt;/p&gt;
&lt;p&gt;amp moved away from GPT-5 as driver and now calls it their &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ampcode.com/news/gpt-5-oracle&quot;&gt;“oracle”&lt;/a&gt;. Meanwhile I use codex and basically constantly work with the smarter model, the oracle. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/btibor91/status/1976299256383250780&quot;&gt;Yes, there are benchmarks&lt;/a&gt;, but given the skewed usage numbers, I don’t trust them. codex gets me far better results than amp. I have to give them kudos tho for session sharing, they push some interesting ideas ahead.&lt;/p&gt;
&lt;p&gt;Factory, unconvinced. Their videos are a bit cringe, I do hear good things in my timeline about it tho, even if images aren’t supported (yet) and they have the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/badlogicgames/status/1977103325192667323&quot;&gt;signature flicker&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Cursor… it’s tab completion model is industry leading, if you still write code yourself. I use VS Code mostly, I do like them pushing things like browser automation and plan mode tho. I did experiment with GPT-5-Pro but &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1976226900516209035&quot;&gt;Cursor still has the same bugs that annoyed me back in May&lt;/a&gt;. I hear that’s being worked on tho, so it stays in my dock.&lt;/p&gt;
&lt;p&gt;Others like Auggie were a blip on my timeline and nobody ever mentioned them again. In the end they all wrap either GPT-5 and/or Sonnet and are replaceable. RAG might been helpful for Sonnet, but GPT-5 is so good at searching at you don’t need a separate vector index for your code.&lt;/p&gt;
&lt;p&gt;The most promising candidates are opencode and crush, esp. in combination with open models. You can totally use your OpenAI or Anthropic sub with them as well (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977286197375647870&quot;&gt;thanks to clever hax&lt;/a&gt;), but it’s questionable if that is allowed, and what’s the point of using a less capable harness for the model optimized for codex or Claude Code.&lt;/p&gt;
&lt;h3&gt;What about $openmodel&lt;/h3&gt;
&lt;p&gt;I keep an eye on China’s open models, and it’s impressive how quickly they catch up. GLM 4.6 and Kimi K2.1 are strong contenders that slowly reach Sonnet 3.7 quality, I don’t recommend them as &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/imfeat7/status/1977246145278583258&quot;&gt;daily driver&lt;/a&gt; tho.&lt;/p&gt;
&lt;p&gt;The benchmarks only tell half the story. IMO agentic engineering moved from “this is crap” to “this is good” around May with the release of Sonnet 4.0, and we hit an even bigger leap from good to “this is amazing” with gpt-5-codex.&lt;/p&gt;
&lt;h3&gt;Plan Mode &amp;amp; Approach&lt;/h3&gt;
&lt;p&gt;What benchmarks miss is the strategy that the model+harness pursue when they get a prompt. codex is far FAR more careful and reads much more files in your repo before deciding what to do. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/thsottiaux/status/1975565380388299112&quot;&gt;It pushes back harder when you make a silly request.&lt;/a&gt; Claude/other agents are much more eager and just try &lt;em&gt;something&lt;/em&gt;. This can be mitigated with plan mode and rigorous structure docs, to me that feels like working around a broken system.&lt;/p&gt;
&lt;p&gt;I rarely use big plan files now with codex. codex doesn’t even have a dedicated plan mode - however it’s so much better at adhering to the prompt that I can just write “let’s discuss” or “give me options” and it will diligently wait until I approve it. No harness charade needed. Just talk to it.&lt;/p&gt;
&lt;h3&gt;But Claude Code now has &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/news/claude-code-plugins&quot;&gt;Plugins&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Do you hear that noise in the distance? It’s me sigh-ing. What a big pile of bs. This one really left me disappointed in Anthropic’s focus. They try to patch over inefficiencies in the model. Yes, maintaining good documents for specific tasks is a good idea. I keep a big list of useful docs in a docs folder as markdown.&lt;/p&gt;
&lt;h3&gt;But but Subagents !!!1!&lt;/h3&gt;
&lt;p&gt;But something has to be said about this whole dance with subagents. Back in May this was called subtasks, and mostly a way to spin out tasks into a separate context when the model doesn’t need the full text - mainly a way to parallelize or to reduce context waste for e.g. noisy build scripts. Later they rebranded and improved this to subagents, so you spin of a task with some instructions, nicely packaged.&lt;/p&gt;
&lt;p&gt;The use case is the same. What others do with subagents, I usually do with separate windows. If I wanna research sth I might do that in a separate terminal pane and paste it to another one. This gives me complete control and visibility over the context I engineer, unlike subagents who make it harder to view and steer or control what is sent back.&lt;/p&gt;
&lt;p&gt;And we have to talk about the subagent Anthropic recommends on their blog. Just look at this &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/wshobson/agents/blob/main/plugins/llm-application-dev/agents/ai-engineer.md&quot;&gt;“AI Engineer” agent&lt;/a&gt;. It’s an amalgamation of slop, mentioning GPT-4o and o1 for integration, and overall just seems like an autogenerated soup of words that tries to make sense. There’s no meat in there that would make your agent a better “AI engineer”.&lt;/p&gt;
&lt;p&gt;What does that even mean? If you want to get better output, telling your model “You are an AI engineer specializing in production-grade LLM applications” will not change that. Giving it documentation, examples and do/don’t helps. I bet that you’d get better result if you ask your agent to “google AI agent building best practices” and let it load some websites than this crap. You could even make the argument that this slop is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/IanIsSoAwesome/status/1976662563699245358&quot;&gt;context poison&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How I write prompts&lt;/h2&gt;
&lt;p&gt;Back when using claude, I used to write (ofc not, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1978104202820812905&quot;&gt;I speak&lt;/a&gt;) very extensive prompts, since this model “gets me” the more context I supply. While this is true with any model, I noticed that my prompts became significantly shorter with codex. Often it’s just 1-2 sentences + &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977175451408990379&quot;&gt;an image&lt;/a&gt;. The model is incredibly good at reading the codebase and just gets me. I even sometimes go back to typing since codex requires so much less context to understand.&lt;/p&gt;
&lt;p&gt;Adding images is an amazing trick to provide more context, the model is really good at finding exactly what you show, it finds strings and matches it and directly arrives at the place you mention. I’d say at least 50% of my prompts contain a screenshot. I rarely annotate that, that works even better but is slower. A screenshot takes 2 seconds to drag into the terminal.&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wisprflow.ai/&quot;&gt;Wispr Flow&lt;/a&gt; with semantic correction is still king.&lt;/p&gt;
&lt;h2&gt;Web-Based Agents&lt;/h2&gt;
&lt;p&gt;Lately I experimented again with web agents: Devin, Cursor and Codex. Google’s Jules looks nice but was really annoying to set up and Gemini 2.5 just isn’t a good model anymore. Things might change soon once we get &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/cannn064/status/1973415142302830878&quot;&gt;Gemini 3 Pro&lt;/a&gt;. The only one that stuck is codex web. It also is annoying to setup and broken, the terminal currently &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1974798735055192524&quot;&gt;doesn’t load correctly&lt;/a&gt;, but I had an older version of my environment and made it work, with the price of slower wramp-up times.&lt;/p&gt;
&lt;p&gt;I use codex web as my short-term issue tracker. Whenever I’m on the go and have an idea, I do a one-liner via the iOS app and later review this on my Mac. Sure, I could do way more with my phone and even review/merge this, but I choose not to. My work is already addictive enough as-is, so when I’m out or seeing friends, I don’t wanna be pulled in even more. Heck, I say this as someone &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibetunnel-first-anniversary&quot;&gt;who spent almost two months building a tool to make it easier to code on your phone&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Codex web didn’t even count towards your usage limits, but &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1976292221390553236&quot;&gt;these days sadly are numbered&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Agentic Journey&lt;/h2&gt;
&lt;p&gt;Let’s talk about tools. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://conductor.build/&quot;&gt;Conductor&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.terragonlabs.com/&quot;&gt;Terragon&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1973132707707113691&quot;&gt;Sculptor&lt;/a&gt; and the 1000 other ones. Some are hobby projects, some are drowning in VC money. I tried so many of them. None stick. IMO they work around current inefficiencies and promote a workflow that just isn’t optimal. Plus, most of them hide the terminal and don’t show everything the model shows.&lt;/p&gt;
&lt;p&gt;Most are thin wrappers around Anthropic’s SDK + work tree management. There’s no moat. And I question if you even want easier access to coding agents on your phone. The little use case these did for me, codex web fully covers.&lt;/p&gt;
&lt;p&gt;I do see this pattern tho that almost every engineer goes through a phase of building their own tools, mostly because it’s fun and because it’s so much easier now. And what else to build than tools that (we think) will make it simpler to build more tools?&lt;/p&gt;
&lt;h2&gt;But Claude Code can Background Tasks!&lt;/h2&gt;
&lt;p&gt;True. codex currently lacks a few bells and whistles that claude has. The most painful omission is background task management. While it should have a timeout, I did see it get stuck quite a few times with cli tasks that don’t end, like spinning up a dev server or tests that deadlock.&lt;/p&gt;
&lt;p&gt;This was one of the reasons I reverted back to claude, but since that model is just so silly in other ways, I now use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977745596380279006&quot;&gt;&lt;code&gt;tmux&lt;/code&gt;&lt;/a&gt;. It’s an old tool to run CLIs in persistent sessions in the background and there’s plenty world knowledge in the model, so all you need to do is “run via tmux”. No custom agent md charade needed.&lt;/p&gt;
&lt;h2&gt;What about MCPs&lt;/h2&gt;
&lt;p&gt;Other people wrote plenty about MCPs. IMO most are something for the marketing department to make a checkbox and be proud. Almost all MCPs really should be clis. I say that as someone &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/claude-code-mcp&quot;&gt;who wrote 5 MCPs myself&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I can just refer to a cli by name. I don’t need any explanation in my agents file. The agent will try $randomcrap on the first call, the cli will present the help menu, context now has full info how this works and from now on we good. I don’t have to pay a price for any tools, unlike MCPs which are a constant cost and garbage in my context. Use GitHub’s MCP and see 23k tokens gone. Heck, they did make it better because it was almost 50.000 tokens when it first launched. Or use the &lt;code&gt;gh&lt;/code&gt; cli which has basically the same feature set, models already know how to use it, and pay zero context tax.&lt;/p&gt;
&lt;p&gt;I did open source some of my cli tools, like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/bslog&quot;&gt;bslog&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/inngest&quot;&gt;inngest&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I do use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.chrome.com/blog/chrome-devtools-mcp&quot;&gt;&lt;code&gt;chrome-devtools-mcp&lt;/code&gt;&lt;/a&gt; these days &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977762275302789197&quot;&gt;to close the loop&lt;/a&gt;. it replaced Playwright as my to-go MCP for web debugging. I don’t need it lots but when I do, it’s quite useful to close the loop. I designed my website so that I can create api keys that allow my model to query any endpoint via curl, which is faster and more token-efficient in almost all use cases, so even that MCP isn’t something I need daily.&lt;/p&gt;
&lt;h2&gt;But the code is slop!&lt;/h2&gt;
&lt;p&gt;I spend about &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1976985959242907656&quot;&gt;20% of my time&lt;/a&gt; on refactoring. Ofc all of that is done by agents, I don’t waste my time doing that manually. Refactor days are great when I need less focus or I’m tired, since I can make great progress without the need of too much focus or clear thinking.&lt;/p&gt;
&lt;p&gt;Typical refactor work is using &lt;code&gt;jscpd&lt;/code&gt; for code duplication, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://knip.dev/&quot;&gt;&lt;code&gt;knip&lt;/code&gt;&lt;/a&gt; for dead code, running &lt;code&gt;eslint&lt;/code&gt;’s &lt;code&gt;react-compiler&lt;/code&gt; and deprecation plugins, checking if we introduced api routes that can be consolidated, maintaining my docs, breaking apart files that grew too large, adding tests and code comments for tricky parts, updating dependencies, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977472427354632326&quot;&gt;tool upgrades&lt;/a&gt;, file restructuring, finding and rewriting slow tests, mentioning modern react patterns and rewriting code (e.g. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://react.dev/learn/you-might-not-need-an-effect&quot;&gt;you might not need &lt;code&gt;useEffect&lt;/code&gt;&lt;/a&gt;). There’s always something to do.&lt;/p&gt;
&lt;p&gt;You could make the argument that this could be done on each commit, I do find these phases of iterating fast and then maintaining and improving the codebase - basically paying back some technical debt, to be far more productive, and overall far more fun.&lt;/p&gt;
&lt;h2&gt;Do you do spec-driven development?&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/the-future-of-vibe-coding&quot;&gt;I used to back in June&lt;/a&gt;. Designing a big spec, then let the model build it, ideally for hours. IMO that’s the old way of thinking about building software.&lt;/p&gt;
&lt;p&gt;My current approach is usually that I start a discussion with codex, I paste in some websites, some ideas, ask it to read code, and we flesh out a new feature together. If it’s something tricky, I ask it to write everything into a spec, give that to GPT-5-Pro for review (via chatgpt.com) to see if it has better ideas (surprisingly often, this greatly improves my plan!) and then paste back what I think is useful into the main context to update the file.&lt;/p&gt;
&lt;p&gt;By now I have a good feeling which tasks take how much context, and codex’s context space is quite good, so often I’ll just start building. Some people are religious and always use a new context with the plan - IMO that was useful for Sonnet, but GPT-5 is far better at dealing with larger contexts, and doing that would easily add 10 minutes to everything as the model has to slowly fetch all files needed to build the feature again.&lt;/p&gt;
&lt;p&gt;The far more fun approach is when I do UI-based work. I often start with sth simple and woefully under-spec my requests, and watch the model build and see the browser update in real time. Then I queue in additional changes and iterate on the feature. Often I don’t fully know how something should look like, and that way I can play with the idea and iterate and see it slowly come to life. I often saw codex build something interesting I didn’t even think of. I don’t reset, I simply iterate and morph the chaos into the shape that feels right.&lt;/p&gt;
&lt;p&gt;Often I get ideas for related interactions and iterate on other parts as well while I build it, that work I do in a different agent. Usually I work on one main feature and some smaller, tangentially related tasks.&lt;/p&gt;
&lt;p&gt;As I’m writing this, I build a new Twitter data importer in my Chrome extension, and for that I reshape the graphql importer. Since I’m a bit unsure if that is the right approach, that one is in a separate folder so I can look at the PR and see if that approach makes sense. The main repo does refactoring, so I can focus on writing this article.&lt;/p&gt;
&lt;h2&gt;Show me your slash commands!&lt;/h2&gt;
&lt;p&gt;I only have a few, and I use them rarely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/commit&lt;/code&gt; (custom text to explain that multiple agents work in the same folder and to only commit your changes, so I get clean comments and gpt doesn’t freak out about other changes and tries to revert things if linter fails)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/automerge&lt;/code&gt; (process one PR at a time, react to bot comments, reply, get CI green and squash when green)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/massageprs&lt;/code&gt; (same as automerge but without the squashing so I can parallelize the process if I have a lot of PRs)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/review&lt;/code&gt; (built-in, only sometimes since I have review bots on GH, but can be useful)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And even with these, usually I just type “commit”, unless I know that there’s far too many dirty files and the agent might mess up without some guidance. No need for charade/context waste when I’m confident that this is enough. Again, you develop intuition for these. I have yet to see other commands that really are useful.&lt;/p&gt;
&lt;h2&gt;What other tricks do you have?&lt;/h2&gt;
&lt;p&gt;Instead of trying to formulate the perfect prompt to motivate the agent to continue on a long-running task, there’s lazy workarounds. If you do a bigger refactor, codex often stops with a mid-work reply. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1978099041884897517&quot;&gt;&lt;strong&gt;Queue up continue messages&lt;/strong&gt;&lt;/a&gt; if you wanna go away and just see it done. If codex is done and gets more messages, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1978111714685063640&quot;&gt;it happily ignores them&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ask the model to &lt;strong&gt;write tests after each feature/fix&lt;/strong&gt; is done. Use the same context. This will lead to far better tests, and likely uncover a bug in your implementation. If it’s purely a UI tweak, tests likely make less sense, but for anything else, do it. AI generally is bad at writing good tests, it’s still helpful tho, and let’s be honest - are you writing tests for every fix you make?&lt;/p&gt;
&lt;p&gt;Ask the model to &lt;strong&gt;preserve your intent&lt;/strong&gt; and “add code comments on tricky parts” helps both you and future model runs.&lt;/p&gt;
&lt;p&gt;When things get hard, prompting and &lt;strong&gt;adding some trigger words&lt;/strong&gt; like “take your time” “comprehensive” “read all code that could be related” “create possible hypothesis” makes codex solve even the trickiest problems.&lt;/p&gt;
&lt;h2&gt;How does your Agents/Claude file look like?&lt;/h2&gt;
&lt;p&gt;I have an Agents.md file with a symlink to claude.md, since Anthropic decided not to standardize. I recognize that this is difficult and sub-optimal, since &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://cookbook.openai.com/examples/gpt-5/gpt-5_prompting_guide&quot;&gt;GPT-5 prefers quite different prompting&lt;/a&gt; than Claude. Stop here and read their prompting guide if you haven’t yet.&lt;/p&gt;
&lt;p&gt;While Claude reacts well to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/Altimor/status/1975752110164578576&quot;&gt;🚨 SCREAMING ALL-CAPS 🚨 commands&lt;/a&gt; that threaten it that it will imply ultimate failure and 100 kittens will die if it runs command X, that freaks out GPT-5. (Rightfully so). So drop all of that and just use words like a human. That also means that these files can’t optimally be shared. Which isn’t a problem to me since I mostly use codex, and accept that the instructions might be too weak for the rare instances where claude gets to play.&lt;/p&gt;
&lt;p&gt;My Agent file is currently ~800 lines long and feels like a collection of organizational scar tissue. I didn’t write it, codex did, and anytime sth happens I ask it to make a concise note in there. I should clean this up at some point, but despite it being large it works incredibly well, and gpt really mostly honors entries there. At least it does far far more often than Claude ever did. (Sonnet 4.5 got better there, to give them some credit)&lt;/p&gt;
&lt;p&gt;Next to git instruction it contains an explanation about my product, common naming and API patterns I prefer, notes about React Compiler - often it’s things that are newer than world knowledge because my tech stack is quite bleeding edge. I expect that I can again reduce things in there with model updates. For example, Sonnet 4.0 really needed guidance to understand Tailwind 4, Sonnet 4.5 and GPT-5 are newer and know about that version so I was able to delete all that fluff.&lt;/p&gt;
&lt;p&gt;Significant blocks are about which React patterns I prefer, database migration management, testing, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1963411717192651154&quot;&gt;using and writing ast-grep rules&lt;/a&gt;. (If you don’t know or don’t use ast-grep as codebase linter, stop here and ask your model to set this up as a git hook to block commits)&lt;/p&gt;
&lt;p&gt;I also experimented and started using &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1973838406099874130&quot;&gt;a text-based “design system”&lt;/a&gt; for how things should look, the verdict is still out on that one.&lt;/p&gt;
&lt;h2&gt;So GPT-5-Codex is perfect?&lt;/h2&gt;
&lt;p&gt;Absolutely not. Sometimes it refactors for half an hour and then &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1973834765737603103&quot;&gt;panics&lt;/a&gt; and reverts everything, and you need to re-run and soothen it like a child to tell it that it has enough time. Sometimes it forgets that it can do &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977695411436392588&quot;&gt;bash commands&lt;/a&gt; and it requires some encouragement. Sometimes it replies &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1976207732534300940&quot;&gt;in russian or korean&lt;/a&gt;. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1974108054984798729&quot;&gt;Sometimes the monster slips and sends raw thinking to bash.&lt;/a&gt; But overall these are quite rare and it’s just so insanely good in almost everything else that I can look past these flaws. Humans aren’t perfect either.&lt;/p&gt;
&lt;p&gt;My biggest annoyance with codex is that it “loses” lines, so scrolling up quickly makes parts of the text disappear. I really hope this is on top of OpenAI’s bug roster, as it’s the main reason I sometimes have to slow down, so messages don’t disappear.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Don’t waste your time on stuff like RAG, subagents, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1977660298367766766&quot;&gt;Agents 2.0&lt;/a&gt; or other things that are mostly just charade. Just talk to it. Play with it. Develop intuition. The more you work with agents, the better your results will be.&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://simonwillison.net/2025/Oct/7/vibe-engineering/&quot;&gt;Simon Willison’s article makes an excellent point&lt;/a&gt; - many of the skills needed to manage agents are similar to what you need when &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/lukasz_app/status/1974424549635826120&quot;&gt;managing engineers&lt;/a&gt; - almost all of these are characteristics of senior software engineers.&lt;/p&gt;
&lt;p&gt;And yes, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/svpino/status/1977396812999688371&quot;&gt;writing good software is still hard&lt;/a&gt;. Just because I don’t write the code anymore doesn’t mean I don’t think hard about architecture, system design, dependencies, features or how to delight users. Using AI simply means that expectations what to ship went up.&lt;/p&gt;
&lt;p&gt;PS: This post is 100% organic and hand-written. I love AI, I also recognize that some things are just better done the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/Alphafox78/status/1975679120898965947&quot;&gt;old-fashioned&lt;/a&gt; way. Keep the typos, keep my voice. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/rohanpaul_ai/status/1977005259567595959&quot;&gt;🚄✌️&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;PPS: Credit for the header graphic goes to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/thorstenball/status/1976224756669309195&quot;&gt;Thorsten Ball&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Claude Code Anonymous</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-09-09-claude-code-anonymous/</link>
    <guid isPermaLink="false">urn:aggr:item:704bb0df1dfd7cbba5bebd6c0a78273d5921e80b</guid>
    <atom:link href="https://steipete.me/posts/2025/claude-code-anonymous/" rel="via" type="text/html"/>
    <pubDate>Tue, 9 Sep 2025 10:00:00 +0000</pubDate>
    <description>Introducing Claude Code Anonymous - a new meetup format for full-breadth developers.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/claude-code-anonymous/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;A few weeks ago I reconnected with an old friend of mine, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://orta.io/&quot;&gt;Orta Therox&lt;/a&gt; and we both discussed how Claude Code completely changed how we approach building software, and that we both have trouble thinking about anything else. Out of this conversation came an idea, and we created a new meetup format, Claude Code Anonymous.&lt;/p&gt;
&lt;p&gt;There were quite a few people who wanted to join this event but were not in London, so in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.highagency.com/&quot;&gt;High-Agency-Style&lt;/a&gt;, I encouraged them to start their own chapters in their cities, and now we have events in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/u5rompg9&quot;&gt;London&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/q50cmcb2&quot;&gt;Vienna&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/5lizqnpz&quot;&gt;Berlin&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/j1fr97j3&quot;&gt;Cologne&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/i37ahi52&quot;&gt;San Francisco&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/h5h322jz&quot;&gt;Delft&lt;/a&gt;, and more cities coming soon. The first London meetup was such a success that we’re doing &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/9qets0h0&quot;&gt;another one on October 13th&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What is Claude Code Anonymous&lt;/h2&gt;
&lt;p&gt;Over the last few months I noticed that not everyone gets hooked the same way with this new way of building software. There’s a certain category of people, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://justin.searls.co/posts/full-breadth-developers/&quot;&gt;full-breadth developers&lt;/a&gt;, folks with both technical and product capabilities, that seem to adopt this the fastest, and I’m jokingly calling them the black eye club, since these people usually have trouble sleeping.&lt;/p&gt;
&lt;p&gt;Claude Code Anonymous is the wordplay on this phenomenon. We wanted to create a space for people to be able to talk through their experiences, and understand that for some it’s hard to speak publicly about using AI / LLM tooling. Talks are not recorded, nor is there a live-stream. The recipe is simple: get a group of like-minded people together, provide a space, drinks, pizza, and focus on the social aspect.&lt;/p&gt;
&lt;h2&gt;Meetup Structure&lt;/h2&gt;
&lt;p&gt;These meetups are structured as a series of lightning talks, ~5m long with talks which start along the lines of: “I was X when Claude Code Y”. So, “I was impressed when Claude Code figured out a bug I had been…”, “I was disappointed when Claude Code deleted my production database” etc.&lt;/p&gt;
&lt;p&gt;Intro + Talks should be one hour sharp, so there’s 2-3h left for social and talking. I’ve been in too many meetups where the talks were too long and people left right after the talks - the goal of Claude Code Anonymous is to meet likeminded people more than watching long talks. And the 5m limit is there so that there’s more people who can show off their work, and it’s a good starting point for a talk later on.&lt;/p&gt;
&lt;h2&gt;Why Claude Code and not X?&lt;/h2&gt;
&lt;p&gt;Don’t take the name too literal. We’re open for any agentic work. We just picked the name Claude Code to target builders and not marketing or HR folks. Calling it “agentic engineering” would attract a different kind of audience. Of course you can talk about any tool you like, be it codex, opencode, Cursor or whatever!&lt;/p&gt;
&lt;p&gt;IMO Claude Code is the defining agent that really started the revolution, so I made it the proprietary eponym. Like Jacuzzi or Tupperware. If you wanna be neutral, call it Agents Anonymous 🕵️♂️&lt;/p&gt;
&lt;h2&gt;How do I start a meetup?&lt;/h2&gt;
&lt;p&gt;It’s really quite simple: Find a space, organize some drinks and spread the word. We’ve been using &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://luma.com/&quot;&gt;Luma&lt;/a&gt; as event platform, and I suggest to let people apply with some questions such as “What are you building” and their social profile.&lt;/p&gt;
&lt;p&gt;So far every event had far more interest than space available, so we’ve been reviewing people to make sure that builders are there - this is not a space for folks who just look to hire or market their product. Same rules apply to lightning talks. Show us what you learned, not what you sell.&lt;/p&gt;
&lt;p&gt;As Code of Conduct we’ve been using the CocoaPods one which can be summarized as: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://cocoapods.org/legal&quot;&gt;“Don’t be a Jerk”&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What Now?&lt;/h2&gt;
&lt;p&gt;Do you want to see Claude Code Anonymous in your city? Make it happen! I’m happy to help with outreach via Twitter, just &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;mailto:steipete@gmail.com&quot;&gt;email&lt;/a&gt; or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete&quot;&gt;DM&lt;/a&gt; me.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Live Coding Session: Building Arena</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-09-06-live-coding-session-building-arena/</link>
    <guid isPermaLink="false">urn:aggr:item:e754b379b061a16b023a736802b1ef1a2b39497b</guid>
    <atom:link href="https://steipete.me/posts/2025/live-coding-session-building-arena/" rel="via" type="text/html"/>
    <pubDate>Sat, 6 Sep 2025 10:00:00 +0000</pubDate>
    <description>Watch me build Arena live - a real-time collaborative coding session exploring AI-powered development workflows.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/live-coding-session-building-arena/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tl;dr: I built and shipped a brand-new feature live (in ~1 hour), watch how I approach agentic engineering with codex&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Join me for an unfiltered look at building Arena - a live coding session where you can see my development process in action, unscripted. Thanks to Eleanor Berger for motivating me to do this video and for organizing the live event!&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.youtube.com/watch?v=68BS5GCRcBo&quot;&gt;YouTube video player&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What we built&lt;/h2&gt;
&lt;p&gt;🤖 Heads up! This is an AI-Assisted summary.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feature:&lt;/strong&gt; &lt;em&gt;Arena&lt;/em&gt; — a new feature in my upcoming project, to see how well 2–4 users from X match&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Input:&lt;/strong&gt; Twitter/X user handles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipeline:&lt;/strong&gt; fetch N tweets per user (shares a 1,000-tweet budget), strip to necessary fields, run profile analysis, then &lt;strong&gt;score compatibility&lt;/strong&gt; (per pair + whole team)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UX:&lt;/strong&gt; user picker + “Analyze” button, results table, cached runs selectable under the search box&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infra touches:&lt;/strong&gt; DB migration for &lt;code&gt;arena_cache&lt;/code&gt;, long-running job in the background, streaming UI, auth-guarded page&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I managed to complete the feature in ~1h, and we got a &lt;strong&gt;pair score of 89&lt;/strong&gt; for me and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/intellectronica&quot;&gt;@intellectronica&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Stack &amp;amp; Setup&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Model workflow:&lt;/strong&gt; codex (OpenAI/GPT-5) for coding sessions; it eagerly &lt;strong&gt;reads the codebase&lt;/strong&gt; and generally “does the right thing” without handheld file lists. I keep a separate Claude-style flow for some web searching, but for repo work codex is the star.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sessions:&lt;/strong&gt; start &lt;em&gt;fresh&lt;/em&gt; for big features, run &lt;strong&gt;multiple agent windows&lt;/strong&gt; in parallel; switch tasks while one is thinking.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Branching:&lt;/strong&gt; work &lt;strong&gt;directly on &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; with &lt;strong&gt;atomic commits&lt;/strong&gt;. Merge conflicts + worktrees cost speed; small, well-scoped commits keep things safe.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tooling:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ghostty&lt;/strong&gt; for terminals, split panes with agents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better Stack&lt;/strong&gt; logging via a tiny &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/bslog&quot;&gt;&lt;strong&gt;&lt;code&gt;bslog&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; CLI&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;&lt;code&gt;xl&lt;/code&gt;&lt;/strong&gt; CLI (&lt;code&gt;curl&lt;/code&gt; wrapper) for quick X API pulls&lt;/li&gt;
&lt;li&gt;Strict &lt;strong&gt;biome&lt;/strong&gt; rules + custom codemods to normalize output&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background worker&lt;/strong&gt; for long jobs (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.inngest.com/&quot;&gt;Inngest&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache table&lt;/strong&gt; to avoid recompute&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Docs ingestion:&lt;/strong&gt; pull only what’s needed, prefer &lt;strong&gt;markdown&lt;/strong&gt; via a crawler over raw HTML to save tokens.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Validation:&lt;/strong&gt; schema validation on inputs; fail fast and surface helpful messages.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tactics that mattered&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep the agent context clean.&lt;/strong&gt; Minimize tool noise and only inject docs when needed; markdown &amp;gt; HTML to conserve context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let codex plan on demand.&lt;/strong&gt; It proposes next steps that are usually solid; green-light them in sequence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache long tasks early.&lt;/strong&gt; Add a table + background job queue before you polish UI; this saves you from re-running expensive analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copy-paste errors verbatim.&lt;/strong&gt; Don’t over-explain; just drop logs in and let the agent fix what broke.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comments as spec.&lt;/strong&gt; Ask for clear intent comments near tricky code; they’re for you and for the agent on the next session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write tests afterwards, not first.&lt;/strong&gt; These agents are great at back-filling tests once the shape exists, and that’s usually enough to catch regressions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Work on &lt;code&gt;main&lt;/code&gt;, commit surgically.&lt;/strong&gt; You still get speed with safety. Backups + Git are your safety net.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid local models for this workload.&lt;/strong&gt; Context and stability matter more than shaving milliseconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLIs beat MCPs.&lt;/strong&gt; A 2-hour CLI wrapper (logs, API pulls) pays for itself and keeps context small.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small “proof” projects unblock big features.&lt;/strong&gt; If streaming or a protocol is hard, build a tiny in-repo example that works, then transplant.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q&amp;amp;A highlights&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Codex over Claude Code?&lt;/strong&gt; Codex actually &lt;strong&gt;reads more of the repo&lt;/strong&gt; without handholding; requires fewer “look here, then there” hints. Claude still useful for search/web, but for coding Codex felt faster end-to-end.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do you branch?&lt;/strong&gt; Not for features at this stage. &lt;strong&gt;Main + disciplined commits&lt;/strong&gt; is faster and - counterintuitively - safer for rapid iteration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual approvals for agent actions?&lt;/strong&gt; No. That becomes “Windows Vista prompts.” Use Git + backups; review diffs; move fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repo prompts &amp;amp; MCP servers?&lt;/strong&gt; Nice idea, but they bloat context and introduce fragility. &lt;strong&gt;Lean instructions + small CLIs&lt;/strong&gt; work better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compaction &amp;amp; long sessions?&lt;/strong&gt; Plan features to &lt;strong&gt;fit the context&lt;/strong&gt;. If you expect many loops (e.g., test repairs), use the flow that compacts well - or split the task.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to read more about my agent workflow, check &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/optimal-ai-development-workflow&quot;&gt;my AI development post here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For more advanced prompting techniques with GPT-5, also check out the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://cookbook.openai.com/examples/gpt-5/gpt-5_prompting_guide&quot;&gt;OpenAI GPT-5 Prompting Guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;New posts, shipping stories, and nerdy links straight to your inbox.&lt;/p&gt;
&lt;p&gt;2× per month, pure signal, zero fluff.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>My Current AI Dev Workflow</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-08-25-my-current-ai-dev-workflow/</link>
    <guid isPermaLink="false">urn:aggr:item:fc92941cfd9fc9d9286ab65049f44140a17b017f</guid>
    <atom:link href="https://steipete.me/posts/2025/optimal-ai-development-workflow/" rel="via" type="text/html"/>
    <pubDate>Mon, 25 Aug 2025 09:00:00 +0000</pubDate>
    <description>Went fully back to Ghostty, VS Code on the side, and Claude Code as my main driver. Here&#39;s what actually works after months of experimentation.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/optimal-ai-development-workflow/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR: Ghostty + Claude Code + minimal tooling = maximum productivity. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1952439355340943534&quot;&gt;Less is more&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It’s time for an &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1960114479028486429&quot;&gt;update on my workflow&lt;/a&gt;, which keeps evolving.&lt;/p&gt;
&lt;p&gt;After &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1955781673670340796&quot;&gt;going all-in on VS Code&lt;/a&gt;, I went fully back to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ghostty.org/&quot;&gt;Ghostty&lt;/a&gt; for my main setup, with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1954691261279146029/photo/1&quot;&gt;VS Code on the side&lt;/a&gt; to look up code and Cursor/GPT-5 for reviews (sometimes also the CLI). I tried &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://zed.dev/&quot;&gt;Zed&lt;/a&gt;, just can’t deal with a terminal that looks &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1955690682477134124/photo/1&quot;&gt;like&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1959259921439969326&quot;&gt;this&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Still extremely happy with my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.dell.com/en-us/shop/dell-ultrasharp-40-curved-thunderbolt-hub-monitor-u4025qw/apd/210-bmdp/monitors-monitor-accessories&quot;&gt;Dell UltraSharp U4025QW&lt;/a&gt; - 3840x1620 makes 4 Claude instances + Chrome all visible without moving windows.&lt;/p&gt;
&lt;p&gt;VS Code’s terminal is too unstable, had plenty freezes when pasting in large amounts of text. Nothing beats Ghostty.&lt;/p&gt;
&lt;h2&gt;Tools and Their Reality&lt;/h2&gt;
&lt;p&gt;Gemini can be great, but its edit tools are &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1942113964231442876&quot;&gt;too&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1955941943218713006/photo/1&quot;&gt;messy&lt;/a&gt;, so using it less and less. GPT-5 for reviewing plans works even better than Gemini.&lt;/p&gt;
&lt;p&gt;Yes, all of these work on main. I tried the whole &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1956340397900177686&quot;&gt;worktree setup&lt;/a&gt;, just slows me down. If you pick areas of work carefully you can work on multiple areas without much cross-pollination.&lt;/p&gt;
&lt;p&gt;Claude often makes a mess but it’s equally great in refactoring and cleaning up. Important to do both to not create too much technical debt.&lt;/p&gt;
&lt;h2&gt;Planning and Context Management&lt;/h2&gt;
&lt;p&gt;Having the initial topic in the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1958903434322526214&quot;&gt;statusline + session ID&lt;/a&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/8396e512171d31e934f0013e5651691e&quot;&gt;gist&lt;/a&gt;) in case I need to switch accounts or restart a session is super helpful.&lt;/p&gt;
&lt;p&gt;Using plan mode and iterating is key. Smaller tasks I do right away, bigger I write in a file and let GPT-5 review.&lt;/p&gt;
&lt;p&gt;Often I manage with small prompts, sometimes I brainstorm and it’s amazing how much sense agents can make out of my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1958233967548850235&quot;&gt;incoherent thoughts&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When I’m not refactoring I usually run 1-2 agents; for cleanup/tests/UI work ~4 seems to be the sweet spot. All depends on the blast radius of the work.&lt;/p&gt;
&lt;h2&gt;The Hard Parts&lt;/h2&gt;
&lt;p&gt;The hardest part is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1958573142726640116/photo/1&quot;&gt;distributed system design&lt;/a&gt;, picking the right dependencies, platforms and a forward-thinking database schema.&lt;/p&gt;
&lt;p&gt;I’ve been building an incredible amount of custom infra, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1958956225800151465&quot;&gt;admin pages&lt;/a&gt;, CLIs to help both me and the agents and that work did speed me up so much. Would have never done that with the old ways.&lt;/p&gt;
&lt;h2&gt;Testing Strategy&lt;/h2&gt;
&lt;p&gt;Bigger changes always get tests. Automated ones usually aren’t great, but the model almost always finds issues when you ask it to write tests IN THE SAME CONTEXT. Context is precious, don’t waste it.&lt;/p&gt;
&lt;h2&gt;Less is More&lt;/h2&gt;
&lt;p&gt;Even &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1958679613489524952&quot;&gt;removed my last MCP&lt;/a&gt;, since Claude sometimes would go off spinning up Playwright unasked when it could simply read the code - which is faster and pollutes the context less.&lt;/p&gt;
&lt;p&gt;Pick services that have CLIs: vercel, psql, gh, axiom. Agents can use them, one line in CLAUDE.md is enough “logs: axiom or vercel cli”. “Database: psql + one example how to load env correctly so the loop is faster”&lt;/p&gt;
&lt;h2&gt;Results and Comparisons&lt;/h2&gt;
&lt;p&gt;Got an insane amount done with this setup. Other CLIs/models still don’t come close. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1954593439347032167&quot;&gt;Codex can’t search&lt;/a&gt; (asking “google best practices” is usually better than context). Cursor/GPT-5 takes FOREVER and doesn’t share its thinking, so it’s hard to steer. GPT-5 also is much more literal in how you have to prompt it - it’s a great model but not the best agent, you have to be more precise and specific.&lt;/p&gt;
&lt;p&gt;Still don’t see how this could be moved to background agents. I steer the models a lot as I notice them drifting off - that’s much harder if they run in the background.&lt;/p&gt;
&lt;p&gt;The new rate limits will go into effect August 28 and that’ll definitely suck. Guess I’ll just pay up. No perfect alternative for now.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Essential Reading for Agentic Engineers - August 2025</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-08-22-essential-reading-for-agentic-engineers-august-2025/</link>
    <guid isPermaLink="false">urn:aggr:item:8d5c9a78969ea9999243fc85d3fc2c8358b2a4e4</guid>
    <atom:link href="https://steipete.me/posts/2025/essential-reading-august-2025/" rel="via" type="text/html"/>
    <pubDate>Fri, 22 Aug 2025 01:00:00 +0000</pubDate>
    <description>Five essential perspectives that cut through AI hype: from developer evolution stages to junior learning crises, productivity reality checks, platform disruption, and MCP server pitfalls.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/essential-reading-august/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The reinvention of software development is happening now.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This edition cuts through the AI hype with five essential perspectives that reveal both the promise and peril of our evolving profession. We track developers’ evolution from AI skeptics to strategists, examine how juniors are losing fundamental skills, reality-check the 10x productivity myths, and explore why both platform monopolies and MCP server proliferation may be dead ends.&lt;/p&gt;
&lt;p&gt;🤖 &lt;em&gt;Heads Up: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/startup-slop&quot;&gt;Summaries are AI-Assisted&lt;/a&gt;. All posts are hand-picked.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Developers, Reinvented&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ashtom.github.io/developers-reinvented&quot;&gt;Read the article&lt;/a&gt; by Thomas Dohmke (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/ashtom&quot;&gt;@ashtom&lt;/a&gt;) • 11 min&lt;/p&gt;
&lt;p&gt;Thomas presents groundbreaking research from interviews with developers who’ve made AI tools central to their workflows, revealing a four-stage evolution that’s reshaping software development from skepticism to strategic AI orchestration.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The four-stage evolution&lt;/strong&gt;: From AI Skeptic (dabbling with completions) to AI Strategist (orchestrating multi-agent workflows with planning and coding models), with each stage requiring different mindsets and skill development&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role transformation in action&lt;/strong&gt;: Advanced practitioners describe their work shifting from “writing code to architecting and verifying implementation work carried out by AI agents”—focusing on delegation (context engineering, prompt design) and verification (validating AI output against objectives)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timeline predictions&lt;/strong&gt;: Half expect 90% AI-written code within 2 years, half within 5 years, but crucially view this as role reinvention rather than replacement: “Maybe we become less code producers and more code enablers. My next title might be Creative Director of Code”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills evolution&lt;/strong&gt;: New emphasis on AI fluency, agent orchestration, human-AI collaboration, and product understanding, while foundational programming knowledge becomes essential for verification and quality control&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Educational implications&lt;/strong&gt;: Computer science curricula must evolve from syntax memorization to AI-assisted problem-solving, teaching students to frame problems, guide AI, and critique solutions rather than just write loops&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Either you have to embrace the AI, or you get out of your career… I think of myself as [a] mediocre engineer, and I feel this AI reset is giving me a chance to build skills that will bring me closer to excellence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The research reveals “realistic optimists” who acknowledge disruption while embracing growth opportunities, focusing less on “time saved” and more on “increasing ambition”—expanding the scope and complexity of what they can accomplish rather than just working faster.&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://nmn.gl/blog/ai-and-learning&quot;&gt;Read the article&lt;/a&gt; by Namanyay Goel (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/NamanyayG&quot;&gt;@NamanyayG&lt;/a&gt;) • 8 min&lt;/p&gt;
&lt;p&gt;Namanyay presents a sobering counterpoint to AI evangelism, examining how AI tools may be creating a generation of developers who can ship code without truly understanding it—trading deep knowledge for quick fixes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The learning crisis&lt;/strong&gt;: Junior developers increasingly rely on AI-generated solutions without understanding the underlying principles, potentially creating a knowledge gap that compounds over time as they advance in their careers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pattern matching vs. understanding&lt;/strong&gt;: AI tools excel at providing working code snippets, but developers miss the crucial struggle phase where deep understanding forms—the difference between knowing how to use a solution and understanding why it works&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-term implications&lt;/strong&gt;: While senior developers leverage AI to amplify existing knowledge, juniors risk building on shaky foundations: “We’re going to pay for this later” when complex debugging or architectural decisions require fundamental understanding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Balanced approach&lt;/strong&gt;: The author advocates for intentional learning practices—interrogating AI solutions, building from scratch periodically, and engaging in meaningful code reviews that go beyond “does it work?” to “why does it work this way?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generational divide&lt;/strong&gt;: A stark contrast emerges between developers who learned pre-AI (with strong fundamentals to verify AI output) and those learning with AI from day one (potentially lacking the foundation to recognize subtle errors or inefficiencies)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;We’re trading deep understanding for quick fixes, and while it feels great in the moment, we’re going to pay for this later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This critical perspective complements the optimistic evolution narrative, highlighting that while AI transforms how we work, the fundamentals of software engineering—problem decomposition, algorithmic thinking, and system design—remain essential for those who want to lead rather than just participate in the AI-augmented future.&lt;/p&gt;
&lt;h2&gt;Reality Check: AI’s Actual Impact on Productivity&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://colton.dev/blog/curing-your-ai-10x-engineer-imposter-syndrome/&quot;&gt;Read the article&lt;/a&gt; by Colton Anglin • 6 min&lt;/p&gt;
&lt;p&gt;Colton delivers a much-needed reality check on the hyperbolic claims of 10x or 100x productivity gains from AI, offering reassurance to engineers experiencing imposter syndrome in the face of aggressive AI marketing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The math doesn’t add up&lt;/strong&gt;: Claims of 10x productivity ignore that most engineering time isn’t spent typing code—it’s spent thinking, debugging, reviewing, and coordinating with teams, none of which AI fundamentally accelerates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real productivity gains&lt;/strong&gt;: AI excels at specific tasks—writing one-off scripts, generating boilerplate, handling repetitive patterns—but these represent a small fraction of engineering work, yielding realistic improvements of perhaps 20-30% in specific contexts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The correction overhead&lt;/strong&gt;: AI-generated code often requires significant manual review and fixing, sometimes taking longer than writing from scratch: “A competent engineer will figure this stuff out in less than a week of moderate AI usage”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Psychological impact&lt;/strong&gt;: The constant barrage of “10x engineer” claims creates unnecessary anxiety and imposter syndrome, when in reality, experienced engineers quickly discover AI’s actual limitations through hands-on use&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finding balance&lt;/strong&gt;: The author advocates for using AI where it genuinely helps (scripts, boilerplate) while maintaining confidence in human skills for complex problem-solving, system design, and the creative aspects of engineering that bring satisfaction&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Real engineers were now 10-100x more productive than I was—the anxiety-inducing thought that the author ultimately debunks through practical experience and mathematical analysis.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This grounded perspective serves as an antidote to both AI evangelism and doom-saying, reminding us that while AI tools are valuable additions to our toolkit, the core skills of software engineering—understanding problems, designing solutions, and building maintainable systems—remain fundamentally human endeavors.&lt;/p&gt;
&lt;h2&gt;The End of Platform Dominance&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.aparker.io/post/3lvjepuyf4q2w&quot;&gt;Read the article&lt;/a&gt; by Austin Parker (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://bsky.app/profile/aparker.io&quot;&gt;@aparker.io&lt;/a&gt;) • 10 min&lt;/p&gt;
&lt;p&gt;Austin presents a visionary thesis on how AI will fundamentally restructure the software industry, shifting from today’s platform monopolies to tomorrow’s ecosystem of small, custom applications built on open protocols.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time economics revolution&lt;/strong&gt;: Platforms rose to dominance when developer time was expensive, but “AI makes time very, very, very cheap”—fundamentally altering the economic equation that has driven software architecture for decades&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The abstraction trap&lt;/strong&gt;: Current software is built at the wrong layer: “We’ve built all of these abstractions, but they’re all built on top of middlemen who would like their 30% cut”—AI enables bypassing these costly intermediaries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom over commodity&lt;/strong&gt;: With AI reducing development time from months to minutes, “Custom applications will become the norm, not the exception”—why use generic tools when bespoke solutions cost virtually nothing to create?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The scale paradox&lt;/strong&gt;: Parker asks the essential question: “Why do I need planet-scale infrastructure to share baby photos with, like, 5 people?”—highlighting how AI enables right-sized solutions instead of one-size-fits-all platforms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware convergence&lt;/strong&gt;: Within 5 years, consumer devices will have onboard AI rivaling today’s best models, enabling truly personal computing where applications are generated on-demand for specific needs&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;All code is technical debt—a provocative reframing that suggests the future isn’t about accumulating codebases but generating disposable, purpose-built applications as needed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This radical vision complements our exploration of role transformation by imagining not just how engineers will work differently, but how the entire software ecosystem will restructure around AI’s ability to make custom development trivially cheap, potentially ending the era of platform monopolies.&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ghuntley.com/allocations/&quot;&gt;Read the article&lt;/a&gt; by Geoffrey Huntley (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/GeoffreyHuntley&quot;&gt;@geoff&lt;/a&gt;) • 10 min&lt;/p&gt;
&lt;p&gt;Geoffrey presents a critical analysis of Model Context Protocol (MCP) servers, warning that the rush to add more tools and integrations is actually degrading AI coding assistant performance rather than improving it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context window economics&lt;/strong&gt;: Every MCP server and tool consumes precious tokens from your LLM’s limited context window—adding more tools means less space for actual code and reasoning, creating a fundamental trade-off that most developers don’t realize&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The allocation paradox&lt;/strong&gt;: “Less is more. The more you allocate into the context window of an LLM… the worse the outcomes you’re going to get”—each additional tool increases cognitive overhead and reduces the AI’s ability to focus on the actual task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool proliferation problems&lt;/strong&gt;: Multiple similar tools create non-deterministic behavior where the LLM struggles to choose between overlapping capabilities, leading to confusion and errors rather than enhanced functionality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security nightmares&lt;/strong&gt;: Third-party MCP servers introduce supply chain risks and potential attack vectors—malicious actors could inject context that manipulates AI behavior, creating a new class of security vulnerabilities enterprises aren’t prepared for&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strategic recommendations&lt;/strong&gt;: Limit MCP servers to essential tools, prefer first-party integrations from trusted vendors, and consider dynamically enabling/disabling servers based on specific workflow stages rather than loading everything at once&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The proliferation of MCP servers is creating a tragedy of the commons in your context window—everyone wants their tool included, but the cumulative effect is making the AI assistant worse, not better.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This technical deep-dive challenges the current enthusiasm around MCP servers, arguing that restraint and strategic thinking about context allocation will deliver better results than the current “more is better” approach that’s dominating the ecosystem.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This builds on insights from my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/essential-reading-july-2025&quot;&gt;July 2025 collection&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/essential-reading&quot;&gt;original Essential Reading&lt;/a&gt; with fresh perspectives on the evolving identity of software engineers in the AI era.&lt;/em&gt;&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Just One More Prompt</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-08-19-just-one-more-prompt/</link>
    <guid isPermaLink="false">urn:aggr:item:08b4327b3075deea52326d18ae9a78ff5971936b</guid>
    <atom:link href="https://steipete.me/posts/just-one-more-prompt/" rel="via" type="text/html"/>
    <pubDate>Tue, 19 Aug 2025 08:00:00 +0000</pubDate>
    <description>Hi, my name is Peter and I&#39;m a Claudoholic. A reflection on AI addiction, extreme work culture, and the blurry line between productivity and obsession in the age of agentic engineering.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/just-one-more-prompt/hero.jpg&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;This is my writeup for the talk I gave at &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;http://claudeanon.club/&quot;&gt;Claude Code Anonymous meeting in London&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Hi, my name is Peter and I’m a Claudoholic. I’m addicted to agentic engineering. And sometimes I just vibe-code. Most of you know me as &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twitter.com/steipete&quot;&gt;@steipete&lt;/a&gt; on Twitter. I was an iOS developer/Entrepreneur in a previous life, and now I do… everything. I work on a ton of projects.&lt;/p&gt;
&lt;p&gt;So. Now you might understand why we call it Claude Code Anonymous. This meetup is for builders. For &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://justin.searls.co/posts/full-breadth-developers/&quot;&gt;full-breadth developers&lt;/a&gt;. For the people who see the revolution and can’t sleep anymore.&lt;/p&gt;
&lt;p&gt;Since I’m one of the organizers and knee-deep in AI, I’ve thought quite hard what I wanna talk about in these 5 minutes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My latest strategies how to best use agents?&lt;/li&gt;
&lt;li&gt;My adventures in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/self-hosting-ai-models&quot;&gt;running open models on Nvidia’s H200&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1952763187553513658&quot;&gt;Alternative agents&lt;/a&gt; like opencode, charm?&lt;/li&gt;
&lt;li&gt;A preview of my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/speaking&quot;&gt;upcoming talk&lt;/a&gt; “You can just do things?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yes, you can just do things. That’s the beauty and the problem. Once you realize how powerful these agents are - this realization that you can finally build everything you ever wanted to build. All these ideas and side projects that you’ve been thinking about for years, but never had the time to do.&lt;/p&gt;
&lt;p&gt;And it doesn’t stop. The more I build, the more ideas for projects I get. And then I worked on multiple projects at the same time, manically yelling at my computer in the middle of the night, because who even types anymore?&lt;/p&gt;
&lt;p&gt;AI was supposed to save time, yet I work more than ever before, I have more FOMO than ever before. I dedicate pretty much all my waking time to this, yet it doesn’t feel enough. One week in AI feels like a month in the real world.&lt;/p&gt;
&lt;p&gt;I’m fun at parties when I tell people about the coming AGI apocalypse, and the 1000 stories of moments where AI just abso - fucking - lutely blew my mind. Don’t get me wrong - I am having a blast. I learned so so much, I built incredibly cool stuff, and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/finding-my-spark-again&quot;&gt;my spark is back&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You might not realize how important that was for me. I burned out after selling my company in 2021 and basically didn’t touch my computer for 3 years. I only used my phone… like a normie! So, having found my way back, the pendulum did swing heavily in the other direction.&lt;/p&gt;
&lt;p&gt;The last few months feel like a blur, and I’m on a new journey how to better control my slot machine addiction. Honestly, I’m failing quite hard. I’m having way too much fun here, and there are all these ideas in my head that need to be codified.&lt;/p&gt;
&lt;p&gt;I know I’m not alone, when I text my friends at 4am and they are also still up. I call them the Black Eye Club. And &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.reddit.com/r/ClaudeAI/comments/1ljurdc/is_this_kind_of_addiction_normal_with_you_claude/?share_id=zX0B6-zPByvTq1GKeRZvq&quot;&gt;on Reddit&lt;/a&gt; there’s plenty of reports as well, about people that can’t sleep anymore.&lt;/p&gt;
&lt;p&gt;Gergely Orosz wrote in his &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://blog.pragmaticengineer.com/new-trend-extreme-hours-at-ai-startups/&quot;&gt;Pragmatic Engineer Newsletter&lt;/a&gt; just a few days ago:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“New trend: extreme hours for AI startup workers. Pulling 80+ hour work weeks – including weekends – is becoming the norm across AI startups, and is unlikely to stop while AI is so hot.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I also see this as a theme that’s industry-wide. Cognition CEO Scott Wu &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/ScottWu46/status/1952776198947520659&quot;&gt;posted on Twitter&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Cognition has an extreme performance culture. We routinely are at the office through the weekend and do some of our best work late into the night. Many of us literally live where we work.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kennan Davison from Icon, an AI Marketing generator &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/kennandavison/status/1899505804677677188&quot;&gt;tweeted&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;why do 6 [days of work] when you can do 7&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Google cofounder Sergey Brin &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://nypost.com/2025/02/28/business/googles-sergey-brin-says-60-hours-per-week-in-office-is-sweet-spot-of-productivity-as-ai-race-heats-up&quot;&gt;told staff in the tech giant’s AI unit&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“60 hours a week is the sweet spot of productivity”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I allowed myself to be sucked into the vortex for a while. This is exactly how I started my last company. I don’t believe you can do that with “regular” working hours. Or to be more specific, I can’t do that with 40h weeks. I work in waves, a period of very intense work, followed by a period of slacking off. Heck, I even know which game I’m going to enjoy next, once I manage to rip myself away from the slot machines.&lt;/p&gt;
&lt;p&gt;One of my passion projects is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal&quot;&gt;VibeTunnel&lt;/a&gt;, a Mac app that tunnels terminal cli’s to the web, so you can talk to your claude or gemini or codex or whatever straight from your phone. I realized I literally built something so I have better access to my drug. It’s really cool btw.&lt;/p&gt;
&lt;p&gt;I also realize that it’s time now to slow down a bit, as it’s gotten to an unhealthy level. A tiny yet meaningful tweak I did the other day was &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/8396e512171d31e934f0013e5651691e&quot;&gt;adding the session time to Claude’s status line&lt;/a&gt;. Serves as a reminder that time is just FLYING when you’re in the flow.&lt;/p&gt;
&lt;p&gt;So here it is. My story. May it serve as a warning for some here that aren’t mainlining cli just yet. If you love to build things, if you are a full-breadth developer, you are at risk. I’d love to hear your strategies after our talk round is done.&lt;/p&gt;
&lt;p&gt;Thanks for letting me share.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Poltergeist: The Ghost That Keeps Your Builds Fresh</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-08-05-poltergeist-the-ghost-that-keeps-your-builds-fresh/</link>
    <guid isPermaLink="false">urn:aggr:item:f3da15d401dd15ce46932374a91437d28dbb1ee3</guid>
    <atom:link href="https://steipete.me/posts/2025/poltergeist-ghost-keeps-builds-fresh/" rel="via" type="text/html"/>
    <pubDate>Tue, 5 Aug 2025 14:00:00 +0000</pubDate>
    <description>Meet Poltergeist: an AI-friendly universal build watcher that auto-detects and rebuilds any project—Swift, Rust, Node.js, CMake, or anything else—the moment you save a file. Zero config, just haunting productivity.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/poltergeist-ghost-keeps-builds-fresh/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;http://polter.build&quot;&gt;Poltergeist&lt;/a&gt; is an AI-friendly universal file-watcher that auto-detects any project and rebuilds them as soon as a file has been changed. It’s &lt;code&gt;npm run dev&lt;/code&gt; for native apps, with automatic configuration, notifications and a smart build queue.&lt;/p&gt;
&lt;h2&gt;The Story&lt;/h2&gt;
&lt;p&gt;In agentic engineering, loop iteration speed is everything. While building &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://peekaboo.boo&quot;&gt;Peekaboo&lt;/a&gt;, a macOS OS automation agent/cli/mcp written in Swift, my main friction point is build time. Swift isn’t famous for its fast compiler, and it’s especially noticeable once you start doing more web dev. In the time Swift is calculating dependencies, my TypeScript project already recompiled.&lt;/p&gt;
&lt;p&gt;Worse, agents would sometimes forget to rebuild before testing, leading to debugging sessions on code that was already fixed.&lt;/p&gt;
&lt;p&gt;Poltergeist fixes that. It builds in the background as soon as a file has been changed, accelerating the code/debug loop for both agents and humans.&lt;/p&gt;
&lt;p&gt;So I wrote a quick bash script to watch Swift files and auto-rebuild in the background. Problem solved - for Swift. But then I realized this would be incredibly useful for any kind of project. I already had both the CLI and Mac app building automatically, and I figured out this could work for so many other use cases.&lt;/p&gt;
&lt;p&gt;That’s when I decided to make this more of a thing - and went into another rabbit hole of building tools for my agents so I can be faster building tools for my agents.&lt;/p&gt;
&lt;h2&gt;The Universal Solution&lt;/h2&gt;
&lt;p&gt;I rewrote the entire system in TypeScript with one goal: make it work for any project, any language, any build system. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/poltergeist&quot;&gt;Poltergeist&lt;/a&gt; became a universal file watcher that ‘haunts’ your projects.&lt;/p&gt;
&lt;p&gt;While there are tools like &lt;code&gt;watchexec&lt;/code&gt;, I didn’t find anything that was designed with an agentic engineering flow in mind. Poltergeist even &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/poltergeist/blob/85d8edfdd48ae4c6b30813264ce453d1df78ee83/src/cli.ts#L389&quot;&gt;detects if a human or agent calls it&lt;/a&gt; and adds helpful messages for agents to steer them to correct usage, even without polluting your AGENTS.ms file.&lt;/p&gt;
&lt;p&gt;Everything happens invisibly. Save a file. By the time you’re ready to test, the fresh binary is waiting. For Mac apps, it even quits and relaunches automatically (yah I hear you, that’s a config setting). True hot reload would be nice, but that’s something for the future.&lt;/p&gt;
&lt;h2&gt;Channeling the Spirit&lt;/h2&gt;
&lt;p&gt;I’ve built Poltergeist completely with Claude Code. It’s a build tool and in the beginning I didn’t plan to make it a thing, only once I saw how useful it is, I decided to spend extra time and focus on it to make it its own thing. It started as a bunch of bash scripts, eventually I asked my agent to convert these into TypeScript and then kept iterating on it.&lt;/p&gt;
&lt;p&gt;Sentences like “all autogenerated code” are insofar not meaningful anymore, as I’ve surely written (or rather: talked) 20 pages of prompts to perfect the design. With AI, English is the new programming language, and the fact that it’s TypeScript is more an implementation detail.&lt;/p&gt;
&lt;p&gt;Why TypeScript? Agents can write it extremely well, the iteration flow is amazingly fast, compilation is instant, it’s cross-platform and Watchman has TypeScript bindings for it.&lt;/p&gt;
&lt;h2&gt;Dancing with Agents&lt;/h2&gt;
&lt;p&gt;You’ve already noticed - I love building developer tools and stuff that makes my life easier. Poltergeist seemed like such an obvious idea, that I was surprised that it didn’t exist yet.&lt;/p&gt;
&lt;p&gt;I usually use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/understanding-codebases-with-ai-gemini-workflow&quot;&gt;my spec.md based approach&lt;/a&gt; for new projects, however this one evolved from some bash scripts to a full-blown dev tool, so there’s no spec, just a lot of prompts and iterating.&lt;/p&gt;
&lt;p&gt;Building it almost felt too easy. Agents are extremely good at writing TypeScript and Go, so Claude one-shotted most of my prompts. I use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wisprflow.ai&quot;&gt;WisprFlow&lt;/a&gt; and my prompts are usually quite long ramblings. I learned that talking more and giving &lt;em&gt;reason&lt;/em&gt; to the things you want, really helps agents to understand and build the right thing.&lt;/p&gt;
&lt;p&gt;The process is in most cases: long prompt + plan only ultrathink. Sometimes also: Give me a few options - esp. when I’m unsure what to do.&lt;/p&gt;
&lt;p&gt;I don’t use Claude’s plan mode, simply saying “plan only” works equally well and fits my flow better (and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1952763998266372231&quot;&gt;don’t get me started on subagents&lt;/a&gt;). I often iterate multiple times on a plan before I type “y” to build it.&lt;/p&gt;
&lt;p&gt;After it’s done I usually write “add tests + update docs” - adding tests per feature is much better than trying to add them at the end, plus if you have the feature in your context, writing tests will almost always uncover bugs in the feature implementation, and since the agent has all context it’s the best time to fix it. If I prompt everything at once they usually still stop and seem less focused - thus the explicit separation.&lt;/p&gt;
&lt;p&gt;I add &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/poltergeist/actions/runs/16762891298&quot;&gt;CI early on GitHub&lt;/a&gt; (again, simply ask cc) which is another layer of testing, that way I can make sure the project works with all OSes, not just on my Mac.&lt;/p&gt;
&lt;p&gt;While writing this blog post, I asked Claude about his opinion on my language choice, and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1952748261472641170&quot;&gt;it didn’t give me the answer I expected&lt;/a&gt;. So - you can already guess - I did the obvious thing and let it loop a few hours to rewrite it in Go.&lt;/p&gt;
&lt;h2&gt;From Poltergeist to Poltergohst&lt;/h2&gt;
&lt;p&gt;Porting your project to a different language would have been nuts just a few months ago, these days I can just use agents and let them run in loops until it’s done. I saw this as an experiment to again try out open models and alternative CLIs &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1952748261472641170&quot;&gt;and tweeted about it here&lt;/a&gt; - currently there are still too many bugs around Qwen 3 Coder and GLM 4.5 with OpenCode and Crush so I can’t recommend either.&lt;/p&gt;
&lt;p&gt;They both have potential but just aren’t well-tested yet with open models and you’ll eventually encounter API errors and slowdowns. Which is unfortunate, since Qwen 3 Coder’s 1 Mio context window would have been perfect to absorb the whole project in one go and convert it.&lt;/p&gt;
&lt;p&gt;My process with Claude Code was to convert all important files (sources, tests, not examples) to one big 1.1MB markdown file, and then copy/paste the complete text with the command “convert to Go”. That way you work around the 256KB limit that would make claude only read the file partially and slowly.&lt;/p&gt;
&lt;p&gt;There are many websites to do the GitHub conversion, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://repo2txt.simplebasedomain.com&quot;&gt;I prefer this one&lt;/a&gt; as it makes it easy to select file types and is fast+simple.&lt;/p&gt;
&lt;p&gt;I didn’t use any todo structure for the conversion and simply nudged Claude a few times to continue - models tend to stop eventually (after ~30-60 min loops), no sophisticated prompts were required though.&lt;/p&gt;
&lt;p&gt;While I built Poltergeist mostly with Claude Sonnet, I did the conversion with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/news/claude-opus-4-1&quot;&gt;Opus 4.1&lt;/a&gt;, to celebrate its release. To improve code quality, I searched for &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/ashokallu/26445f706d33308b0f243f76618cb687&quot;&gt;an idiomatic, modern Go guide&lt;/a&gt; and ran a few refactors with that.&lt;/p&gt;
&lt;p&gt;Ultimately I decided against Go, since I’m not super comfortable in it, and with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/jarredsumner/status/1952827266440019986&quot;&gt;Bun’s SPA mode&lt;/a&gt;, startup time is at ~44ms, and it’s perfect to distribute as a single binary (~20 MB compressed) on Homebrew.&lt;/p&gt;
&lt;p&gt;There’s also the ecosystem argument that I didn’t see initially. Poltergeist uses Watchman under the hood which has superb TypeScript bindings, but no official Go bindings, leading to more code to maintain. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/sjansen/watchman&quot;&gt;The only Go binding project&lt;/a&gt; that exists hasn’t been maintained in years.&lt;/p&gt;
&lt;p&gt;I kept this experiment on GitHub - &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/poltergohst&quot;&gt;check out poltergohst&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Seance Time&lt;/h2&gt;
&lt;p&gt;Pick homebrew if you’re on a modern Mac, otherwise npm. The npm version needs &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://facebook.github.io/watchman/docs/install.html&quot;&gt;Watchman&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://nodejs.org/en/download&quot;&gt;Node&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; For modern macOS&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;brew&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; tap steipete/tap&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;brew&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; install poltergeist&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Windows, Linux, Intel Mac (Node 20+)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;npm&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; install&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;g&lt;/span&gt; @steipete/poltergeist&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Auto-detect and configure your project&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;poltergeist&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; init&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; That&amp;#39;s it! (`start` works too, gotta keep the agents happy!)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;poltergeist&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; haunt&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Run your tool (always fresh!)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;polter&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; my-cli&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;help&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oh, before I forget - there’s also a native macOS menu bar app! It still needs some work so I haven’t released it yet, you can totally &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/poltergeist/tree/main/apps/mac&quot;&gt;get the source though&lt;/a&gt; and play with it. With that you’ll always see if Poltergeist is compiling and if your build is red or green. Might just call it Ghostbuster.&lt;/p&gt;
&lt;h3&gt;Catch the Ghost&lt;/h3&gt;
&lt;p&gt;Poltergeist is a new kind of tool that’s built for humans and agents in mind - something we’ll surely see more of in the future. You install it once and then forget about it, since it fades into the background, your agent will use it automatically and you can use it or keep doing things the manual way. It’s the perfect invisible addition - you don’t see it, yet it’s here and helps you get results faster.&lt;/p&gt;
&lt;p&gt;The best tools are invisible until you need them, then indispensable once you have them. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;http://polter.build&quot;&gt;Poltergeist&lt;/a&gt; is both - and it’s just one &lt;code&gt;init&lt;/code&gt; away.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Don&#39;t read this Startup Slop</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-08-02-don-t-read-this-startup-slop/</link>
    <guid isPermaLink="false">urn:aggr:item:a6bd12da1ccf26c0f949210505036f8fdb1104e4</guid>
    <atom:link href="https://steipete.me/posts/2025/startup-slop/" rel="via" type="text/html"/>
    <pubDate>Sat, 2 Aug 2025 14:00:00 +0000</pubDate>
    <description>My website was banned from Lobsters as &#39;startup slop&#39; for using AI agents to help write blog posts. When does tool-assisted writing become slop, and why are we having the wrong conversation about AI in content creation?</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/startup-slop/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;tl:dr: My website is banned on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;http://lobste.rs&quot;&gt;http://lobste.rs&lt;/a&gt; as “startup slop”, because I use agents to help me write parts of my blog posts.&lt;/p&gt;
&lt;h2&gt;Backstory&lt;/h2&gt;
&lt;p&gt;About a month ago I learned that some moderator banned my whole domain because I use agents to help me write some of my blog posts. I make no secret out of that and my repo is open source, you can see exactly how I work and how much effort is in each post, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/steipete.me/pull/106/commits&quot;&gt;down to every git commit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I won’t point to people or social networks, it’s easy enough to find if you really care.&lt;/p&gt;
&lt;p&gt;Funny enough, the trigger was the most popular and influential post on my website: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer&quot;&gt;Claude Code is my Computer&lt;/a&gt;. The article where the whole point was to expand people’s mind in what’s all possible with modern agents - and that it far extends the typical use cases I’ve seen so far.&lt;/p&gt;
&lt;h2&gt;What’s Slop?&lt;/h2&gt;
&lt;p&gt;As a society, it’s time to have a discussion about AI tho. This isn’t just about you or Lobsters - it’s about how we’re going to navigate a world where the tools of creation are rapidly changing.&lt;/p&gt;
&lt;p&gt;A part of me sympathizes - I hate reading AI slop, and if I find someone using AI automation to send me Twitter replies or DMs, I block them immediately. If communication is directly sent to me, is automated and is not marked as being automated and instead a human avatar is used, it’s an insult of my time. My Twitter takes are 100% handcrafted, artisan typed words. I don’t even use WisprFlow there so you get the rawest, most organic experience.&lt;/p&gt;
&lt;p&gt;There’s a difference tho between automated replies and using agents to assist writing. At what point does it matter if I spend 4 hours prompting an agent to craft my thoughts into a post vs writing it manually? What defines slop?&lt;/p&gt;
&lt;p&gt;There’s another argument to make. My day, just like yours, only has 24 hours. And I do need a few of them to sleep because organic machines are fragile. Some posts I care a lot for and write every word myself.&lt;/p&gt;
&lt;p&gt;Others are things I learn, but given the 1000 things I do, I would likely not have cared enough to take my notes and put them online, and they have a dual goal of being documentation for humans and really also a guide for agents (like the one to fix &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/logging-privacy-shenanigans&quot;&gt;logging on macOS&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Would it be a better outcome to stop sharing these things and only allow the posts that each take me half a day to write on, so some people are not hurt in their feelings?&lt;/p&gt;
&lt;h2&gt;The Bigger Picture&lt;/h2&gt;
&lt;p&gt;I say feelings, because I tried to discuss with one of their moderators and was met with so much insult and deprecation of my work, a spiteful mood from a person that don’t even knows me, for something that I put up for free online. Logically that makes no sense. It’s almost as if there’s sth bigger they hate when it comes to AI and I’m just the face they can project that to in that very moment.&lt;/p&gt;
&lt;p&gt;Honestly, with such a reaction I’m not mad that they block me and my work. That’s not a community I wanna be a part of.&lt;/p&gt;
&lt;p&gt;But I wonder if it wouldn’t be better to judge &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/cosimo_rip/status/1952013362214154550&quot;&gt;the value of writing on its own merits.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I mean dang, these days I write more words manually for blog posts than I do for code. Do these folks also get offended when I use agents to generate code? documentation?&lt;/p&gt;
&lt;p&gt;You can create slop with or without agents, as you can create great work with or without. Agents are just another tool.&lt;/p&gt;
&lt;p&gt;claude: commit, push and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/steipete.me/pull/207&quot;&gt;merge PR&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Yes, I asked ChatGPT to make the header graphic extra AI-y, just because.&lt;/em&gt;&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Essential Reading for Agentic Engineers - July 2025</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-08-02-essential-reading-for-agentic-engineers-july-2025/</link>
    <guid isPermaLink="false">urn:aggr:item:fe3d7d5205e8c877439daaa3b107ccf9ca881a02</guid>
    <atom:link href="https://steipete.me/posts/2025/essential-reading-july-2025/" rel="via" type="text/html"/>
    <pubDate>Sat, 2 Aug 2025 01:00:00 +0000</pubDate>
    <description>Fresh insights on AI-assisted development: practical experiences with Claude Code and the evolving landscape of full-breadth developers in the age of AI</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/essential-reading-july/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New perspectives on AI-assisted development from the field.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This edition features four compelling articles that showcase the evolving landscape of agentic engineering: a detailed experience report from a team successfully integrating Claude Code into production workflows, a thought-provoking analysis of how AI tools are reshaping developer career paths, a candid look at AI automation experiments that didn’t work as expected, and a technical deep-dive challenging conventional wisdom about MCP limitations.&lt;/p&gt;
&lt;h2&gt;Six Weeks of Claude Code&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://blog.puzzmo.com/posts/2025/07/30/six-weeks-of-claude-code/&quot;&gt;Read the article&lt;/a&gt; by Orta Therox (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/orta&quot;&gt;@orta&lt;/a&gt;) • 12 min&lt;/p&gt;
&lt;p&gt;Orta shares his experience integrating Claude Code into daily development work at Puzzmo, providing one of the most detailed real-world productivity assessments available. His team completed 15+ significant engineering tasks in just six weeks, demonstrating measurable impact on technical debt resolution and feature development.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Workflow innovations&lt;/strong&gt;: Introduced “Write First, Decide Later” approach for rapid prototyping and parallel development strategies using multiple git clones with different VS Code profiles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quantitative insights&lt;/strong&gt;: While commit/PR metrics didn’t dramatically change, perceived productivity increased significantly—completing tasks like Adium theme recreation in ~2 hours that would normally take much longer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical applications&lt;/strong&gt;: Excelled at React Native to React conversions, system migrations, infrastructure updates, and exploration of experimental features across diverse technical domains&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Team perspective&lt;/strong&gt;: Treated Claude as a “pair programming buddy with infinite time and patience,” running with minimal permissions for maximum flexibility&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Philosophy&lt;/strong&gt;: Compared AI coding to “introduction of photography” in programming—a fundamental shift requiring new approaches but not replacing core engineering skills&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Claude Code has fundamentally changed how we approach technical debt and side projects, enabling rapid exploration and implementation that seemed impossible before.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Full-Breadth Developers&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://justin.searls.co/posts/full-breadth-developers/&quot;&gt;Read the article&lt;/a&gt; by Justin Searls (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/searls&quot;&gt;@searls&lt;/a&gt;) • 15 min&lt;/p&gt;
&lt;p&gt;Justin explores how AI tools are enabling a new archetype of “full-breadth developers” who can work effectively across the entire technology stack, fundamentally challenging traditional specialization models and career development paths.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Paradigm shift&lt;/strong&gt;: AI enables developers to work competently across multiple domains without years of specialization in each, with Justin completing “two months worth of work on Posse Party” in just two days using Claude Code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Career evolution&lt;/strong&gt;: Traditional role segregation between engineering and product is becoming obsolete—successful developers now need to be results-oriented, experiment rapidly, and identify opportunities others miss&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cognitive transformation&lt;/strong&gt;: AI handles syntax, configuration, and boilerplate complexity, freeing developers to focus on higher-level design and product thinking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New skill requirements&lt;/strong&gt;: Success requires strong prompt engineering, system thinking, and the ability to verify AI-generated solutions rather than deep technical specialization&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Democratization&lt;/strong&gt;: Complex tasks that once required specialists become accessible to generalists with AI assistance, creating opportunities for adaptable, multi-skilled developers&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;We’re moving from an era where depth was king to one where breadth plus AI might be the winning combination for creating software that truly matters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Things That Didn’t Work&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://lucumr.pocoo.org/2025/7/30/things-that-didnt-work/&quot;&gt;Read the article&lt;/a&gt; by Armin Ronacher (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/mitsuhiko&quot;&gt;@mitsuhiko&lt;/a&gt;) • 18 min&lt;/p&gt;
&lt;p&gt;Armin provides a candid retrospective on AI coding experiments that failed, offering valuable lessons for developers navigating the AI-assisted development landscape. His honest analysis of what didn’t work provides essential balance to the enthusiasm around AI automation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automation failure modes&lt;/strong&gt;: Documents specific failed experiments with slash commands, hooks, and print mode automation—most pre-built commands went unused due to limitations like unstructured argument passing and lack of file-based autocomplete&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Over-automation dangers&lt;/strong&gt;: Warns that elaborate automation leads to disengagement and actually degrades AI performance, with critical insight that “LLMs are already bad enough as they are, but whenever I lean in on automation I notice that it becomes even easier to disengage”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context over complexity&lt;/strong&gt;: Demonstrates that “simply talking to the machine and giving clear instructions outperforms elaborate pre-written prompts”—flexibility and adaptability matter more than sophisticated workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Human engagement imperative&lt;/strong&gt;: Emphasizes the need to maintain active mental engagement and avoid becoming passive consumers of AI-generated solutions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical principles&lt;/strong&gt;: Only automate consistently performed tasks, manually evaluate automation effectiveness, and be willing to discard ineffective workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The key lesson is that AI is incredibly powerful for execution but still needs human guidance for strategy and quality assurance—automation should amplify human decision-making, not replace it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;MCPs are Boring (or: Why we are losing the Sparkle of LLMs)&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.youtube.com/watch?v=J3oJqan2Gv8&quot;&gt;Watch the video&lt;/a&gt; by Manuel Odendahl (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/programwithai&quot;&gt;@programwithai&lt;/a&gt;) • 32 min&lt;/p&gt;
&lt;p&gt;Manuel presents a provocative technical argument that MCPs artificially limit LLM capabilities by forcing structured tool calls instead of leveraging their superior code generation abilities. His presentation challenges the entire foundation of current agentic development practices with concrete performance data and working implementations.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool calling inefficiency exposed&lt;/strong&gt;: Traditional MCPs waste massive resources—20,000 tokens, $0.50, and 5 minutes for queries that code generation handles in 500 tokens with deterministic results&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic tool creation paradigm&lt;/strong&gt;: Demonstrates how LLMs can generate exactly the tools needed in real-time rather than being constrained by predefined schemas, with live examples showing SQL query optimization and API creation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recursive development potential&lt;/strong&gt;: Introduces “ask LLM to write code that writes code” methodology, enabling infinite tool creation loops where generated code creates libraries, views, and reusable functions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concrete implementation&lt;/strong&gt;: Shows JavaScript sandbox with SQLite and web server libraries that transforms from single eval tool into full CRM application with REST endpoints and web interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance metrics&lt;/strong&gt;: Quantifies improvements—15 tool calls reduced to 1, significant token savings, and 2-3 second execution vs traditional multi-minute workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;LLMs are absolute magic and we should think recursively—if you ask the LLM to do something, ask it to write code to do something, then ask it to write code to write code. They create words that create more words, and ultimately make things happen in the real world.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Coding with LLMs in Summer 2025&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://antirez.com/news/154&quot;&gt;Read the article&lt;/a&gt; by Salvatore Sanfilippo (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/antirez&quot;&gt;@antirez&lt;/a&gt;) • 8 min&lt;/p&gt;
&lt;p&gt;Antirez (creator of Redis) shares practical insights from using LLMs for coding, emphasizing the critical importance of keeping humans “in the loop” while leveraging AI’s transformative capabilities for software development.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Human-guided approach&lt;/strong&gt;: Advocates against “vibe coding” where LLMs handle everything autonomously—developers must provide extensive context, detailed specifications, and remain actively involved in the process&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM capabilities&lt;/strong&gt;: Demonstrates how advanced models like “Gemini 2.5 PRO” and “Claude Opus 4” can eliminate bugs before deployment, enable rapid solution exploration, and accelerate work with unfamiliar technologies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collaborative methodology&lt;/strong&gt;: Treats LLMs as powerful design partners for exploring potential solutions and architectural decisions, while maintaining human oversight and validation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical workflow&lt;/strong&gt;: Recommends manual code transfer between environments, using multiple LLMs for complex problems, and providing rich context to maximize AI effectiveness&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer evolution&lt;/strong&gt;: Positions LLM-assisted coding as a fundamental shift requiring new skills while preserving core engineering judgment and problem-solving abilities&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The key is to use LLMs as incredibly capable assistants that can handle implementation details and exploration, while developers focus on architecture, validation, and maintaining quality standards.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This builds on my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/essential-reading&quot;&gt;original Essential Reading collection&lt;/a&gt; with fresh insights from the field. Continue with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/essential-reading-august-2025&quot;&gt;Essential Reading for Agentic Engineers - August 2025&lt;/a&gt; for perspectives on how AI is fundamentally reshaping developer identity and career paths.&lt;/em&gt;&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Self-Hosting AI Models After Claude&#39;s Usage Limits</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-31-self-hosting-ai-models-after-claude-s-usage-limits/</link>
    <guid isPermaLink="false">urn:aggr:item:316570e7263fa7134efbe6cf3374390122bd21ed</guid>
    <atom:link href="https://steipete.me/posts/2025/self-hosting-ai-models/" rel="via" type="text/html"/>
    <pubDate>Thu, 31 Jul 2025 09:00:00 +0000</pubDate>
    <description>After Claude Pro changed to weekly limits, I explored self-hosting Qwen3-Coder-480B with 400k context windows. Here&#39;s what I learned about costs, alternatives, and why Claude Code still dominates the landscape.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/self-hosting-ai-models/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;When Anthropic changed Claude Max’s subscription model from a 5-hour usage window to weekly limits, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1949901121998508119&quot;&gt;it hit me hard&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Claude Code is still king, Qwen 3 Coder + opencode is a strong contender tho. Have high hopes for Gemini cli and Crush.&lt;/p&gt;
&lt;h2&gt;Claude Code Anonymous&lt;/h2&gt;
&lt;p&gt;I’m definitely part of the 1% that has got &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1948549916604989706&quot;&gt;&lt;strong&gt;a ton&lt;/strong&gt;&lt;/a&gt; of benefit out of their payment model, and I’m not entirely surprised that they’re changing the deal, given the incredible demand for Claude Code. Much like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.reddit.com/r/cursor/comments/1lrc7q8/cursor_pricing_changed_after_12_days/&quot;&gt;Cursor’s recent pricing changes&lt;/a&gt;, this caused a lot of anger in the community, and it also triggered my curiosity on what other options are out there.&lt;/p&gt;
&lt;p&gt;Just to set the record straight tho: While I often worked 16h-days, I never automated Claude Code and was well within their Terms of Service. My Anthropic bill for July is at &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1949908573452193866&quot;&gt;~6000$&lt;/a&gt;, since I’ve been using their GitHub Review bot, and while some people have been extracting the Max API Token to run on GitHub in the background, I opted to not break the rules and pay up… so Anthropic, we cool?&lt;/p&gt;
&lt;h2&gt;Evaluating The Landscape&lt;/h2&gt;
&lt;p&gt;So what are the alternatives? I spend some time testing every promising tool I could find. Claude Code is insofar hard to replace, as it’s this genius blend of amazing model &amp;amp; tooling. But the landscape is changing fast! There’s quite a few contenders out there that are worth a look:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://opencode.ai/&quot;&gt;opencode&lt;/a&gt; In my tests, opencode is the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951288839814725862&quot;&gt;most promising&lt;/a&gt; alternative currently. It supports pretty much all providers and gets better every day. It has an optimized prompt for Qwen 3 Coder, and produces good results. I’ll definitely be using this more.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/charmbracelet/crush&quot;&gt;charm crush&lt;/a&gt; I &lt;em&gt;really&lt;/em&gt; want to love this one. It’s gorgeous and fun to use. No surprise, it’s backed by a company that makes fun cli tools and that shows. Crush was released just yesterday and it’s still early. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951288839814725862&quot;&gt;It has problems with the edit tool and sometimes just gets stuck&lt;/a&gt;, at least when using it with Qwen 3 Coder. I had to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/charmbracelet/crush/pull/414&quot;&gt;send a PR&lt;/a&gt; to make it work with self hosted inference. There’s a whole team working on it so my hopes are up. If you use Anthropic’s models, I’m sure it performs much better. Personal deal-breaker: The non-working text selection. You can’t click on links either. I use this a lot so it’d really disruptive for my workflow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/musistudio/claude-code-router&quot;&gt;Claude Code with claude-code-router&lt;/a&gt; Clever hack that patches Claude Code to use different models. Since CC is optimized for Anthropic’s models, tool use and overall results aren’t as good as with Sonnet or Opus. I didn’t explore this further.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/cline/cline&quot;&gt;Cline&lt;/a&gt; I used Cline for testing the model, but since it’s a VS Code extension and not a cli, it doesn’t fit my workflow, and generally &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951062013913727332&quot;&gt;I haven’t been impressed by it&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ampcode.com/&quot;&gt;amp&lt;/a&gt; Amp is a very opinionated CLI. It uses Claude Sonnet and has a tool that can invoke OpenAI’s o3. Pricing is forwarded without markup. You can’t use your subscription here, so until now, it wasn’t interesting. It’s hyped, in my tests &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951059420193968581&quot;&gt;it didn’t impress me tho&lt;/a&gt;. Some people swear by it tho, and it’s certaily more token efficient than Claude Code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/google-gemini/gemini-cli&quot;&gt;Gemini CLI&lt;/a&gt; / &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/QwenLM/qwen-code&quot;&gt;Qwen Code&lt;/a&gt; I have high hopes for Gemini CLI, and with it, the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951058436030079159&quot;&gt;Qwen Code fork&lt;/a&gt;. Gemini is REALLY FAST. I use it to debug tricky issues that need a large context space. Tool calling is still &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1942113964231442876&quot;&gt;very broken “I’m just a file”&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951301660950581709&quot;&gt;it can be really lazy&lt;/a&gt;, however it’s open source and a lot of folks are working on making it better. As for the Quen Code fork, we’ll see if they keep up with the fast-paced development of Gemini or abandon it. I worry it’ll be the latter, at least from looking at the current pace of updates.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/RooCodeInc/Roo-Code&quot;&gt;so&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://kiro.dev/&quot;&gt;many&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.augmentcode.com/changelog/auggie-cli&quot;&gt;more&lt;/a&gt; tools out there in all colors and sizes, and it’s really impressive how important tooling &amp;amp; the right system prompt are (e.g. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951080437188624564&quot;&gt;RooCode drawing diagrams!&lt;/a&gt;). IMO that’s Claude Code’s strength: it’s optimized for just one model, and it shows.&lt;/p&gt;
&lt;h2&gt;The Infrastructure Journey&lt;/h2&gt;
&lt;p&gt;My main goals for this exploration are twofold: I don’t wanna be too dependent on one company &amp;amp; tool (Anthropic), and I want to have a less costy solution for my heavy use.&lt;/p&gt;
&lt;p&gt;A few months ago I bought a Mac Studio with 512GB ram, top of the line, to experiment with models (and because it’s prettyyh). Turns out, back then there just wasn’t anything great out there that could compete with Opus &amp;amp; Gemini. And then came Deepseek, and with that the realization that even the 512GB won’t get me very far.&lt;/p&gt;
&lt;p&gt;Yes, I can run Deepseek Coder V2 at ~25 tok/s or a quantified version of R1 at ~8–15 tok/s, but that’s not fun, and 128k context size is quite a bit of a downgrade compared to Claude’s 200k or Gemini’s 1Mio context. There’s ways to connect multiple Studio’s for more performance and/or less compressed models, but then we’re talking 30k$+ for the setup.&lt;/p&gt;
&lt;p&gt;Hardware is one thing - we also need capable models. These days, chinese labs releases better and more capable models on an almost weekly basis. The release of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://qwenlm.github.io/blog/qwen3-coder/&quot;&gt;Qwen3-Coder-480B&lt;/a&gt; got my attention, as it’s the first model that achieves a similar score on SWE-bench as Claude Sonnet 4. Plus, it has a native 256k token context window that can stretch to 1M tokens with YaRN.&lt;/p&gt;
&lt;p&gt;Running this needs some beefy hardware. NVidia’s H200 (Hopper refresh) is currently the best top-of-the-line choice. There’s also B200 (Blackwell), but they are usually even more expensive to rent, and software support is still quite weak.&lt;/p&gt;
&lt;p&gt;I’ve been experimenting with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.primeintellect.ai/&quot;&gt;Prime Intellect&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vast.ai/&quot;&gt;Vast.ai&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://datacrunch.io/&quot;&gt;DataCrunch&lt;/a&gt;. Prices for 8xH200 machines range from $5-30/hour, depending on location, availability and if you rent a spot instance or a regular one. Now spot instances are instances that are available temporarily. Whenever data centers need more capacity, they will simply deprovision it and use it again.&lt;/p&gt;
&lt;p&gt;In my tests, if you get a H200 spot instance it’ll likely disappear before you are finished setting it up. They can be incredibly cheap, but it aint fun when you suddenly have to stop working and set up a new rack because of peak demand. We’re talking $6/h for spot instances vs $14/h for a regular 8xH200 rack, at least on DataCrunch, which seems to be one of the cheapest (while still reliable in my tests) options out there. On vast.ai, a similar setup is more like ~$26/h, and double that again if you look at AWS.&lt;/p&gt;
&lt;p&gt;The setup I’m currently running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;8x&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; H200 (1128GB GPU VRAM&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-post-cmd syntax-shell&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;176&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; CPU, 1450GB RAM&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;Ubuntu&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; 24.04, CUDA 12.8&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;1&lt;/span&gt;&lt;/span&gt;5/h&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With my rig I could set up the FP8&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fnref-1&quot; data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt;-version of Qwen3 Coder 480B with a 400k context. FP8 means smaller float’s, so you lose a few percent accuracy compared to the FP16-version, which is used for the benchmarks. For the Qwen model these losses are insignificant though, and it’s a big performance win, plus squeezing in the FP16 model would leave us with maybe 32k tokens of context.&lt;/p&gt;
&lt;p&gt;Memory is absolutely everything at this scale. The KV cache&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-2&quot; id=&quot;fnref-2&quot; data-footnote-ref&gt;2&lt;/a&gt;&lt;/sup&gt; (that’s what stores the conversation context) eats about 4.2MB per token when using FP16 precision. If you want a 1M token context window, you’d need 17-30 H200 GPUs. That’s why I “only” got 400k tokens with 8 GPUs.&lt;/p&gt;
&lt;p&gt;Setup is tricky, but since &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer&quot;&gt;Claude Code is my computer&lt;/a&gt;, it was mostly a bit of prompting and about half an hour waiting until the rig was ready - at least for H200.&lt;/p&gt;
&lt;p&gt;Currently sparse instances of B200 are an incredibly good deal (~$4/h) and they also been extremely stable in my tests. I also understand why: this hardware is so new that you’ll unlikely succeed running models efficiently on it. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951217528161567193&quot;&gt;I spent all day on this and got it partially running&lt;/a&gt;, but at no point was it faster than a 8xH200 rig. That will change in a few weeks tho as software catches up.&lt;/p&gt;
&lt;p&gt;Insane if you think about it: The 3 8xB200 rack hardware I rented here is worth about 2 Mio Dollar.&lt;/p&gt;
&lt;h2&gt;Cost Analysis&lt;/h2&gt;
&lt;p&gt;With the technical setup sorted, let’s get to the question everyone’s really asking about: Money.&lt;/p&gt;
&lt;p&gt;Running this setup costs $15/h per hour. That’s ~$360 per day if you run it 24/7, or about $11,000/month. You could recreate the machine every day to save money, but setting this up also takes 30-60 minutes each morning. That would drive cost down to ~$120 a day and ~$2,600 a month (if we just count work days, but who doesn’t work on the weekends?)&lt;/p&gt;
&lt;p&gt;Those numbers probably made you wince. So here’s the honest assessment after burning through all that cash:&lt;/p&gt;
&lt;h2&gt;Is It Worth It?&lt;/h2&gt;
&lt;p&gt;Simple answer: No. You can’t stop and easily restart instances, at least not with the hardware provider I tested. Restarted instances will be scheduled, but there’s no guarantee you get a spot in that data center, so it might take days or weeks until your setup reboots.&lt;/p&gt;
&lt;p&gt;What about performance? I naively thought that I’d get better performance with my personal 8xH200 rig, so I ran some tests. And since I couldn’t find a simple benchmark tool, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1951288839814725862&quot;&gt;I vibed one in go&lt;/a&gt;. Turns out, Alibaba is slightly faster than my best optimized 8xH200 rig. Maybe that changes once someone gets the B200 racks working.&lt;/p&gt;
&lt;p&gt;If you want REAL performance and also a damn good deal, check out &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.cerebras.ai/blog/qwen3-coder-480b-is-live-on-cerebras&quot;&gt;Cerebra’s new offering&lt;/a&gt; that came out today. Cerebras Code Max, 5000 messages per day for $200/month. About 20x faster than Claude 4 Sonnet.&lt;/p&gt;
&lt;p&gt;You can rent the 1 Mio-context version of Qwen 3 Coder on Alibaba for &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.alibabacloud.com/help/en/model-studio/models#8e453767fbkka&quot;&gt;$1-$6 In / $5-$60 Out per Million tokens&lt;/a&gt;, depending how much context you’re using. That’s a lot of tokens you have to burn to make self-hosting cost effective. And yes, a self-hosted runner could be shared, but the economics are still against you, and you quickly run into bottlenecks if more than one person accesses your server concurrently.&lt;/p&gt;
&lt;p&gt;Compare to Anthropic: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/pricing#api&quot;&gt;$15 / $75 for Opus, $3 / $15 for Sonnet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Google’s Gemini 2.5 Pro: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ai.google.dev/gemini-api/docs/pricing&quot;&gt;$1.25-$2.50 / $10-$15&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To give you an idea, on an intense day I need maybe 500 Mio tokens. Calculating the price is difficult tho, since many tokens are cached as you work through a session. The cost for that, when using Opus, would be about $1000; for Sonnet about $200 - that’s similar to running my rig for 8h. Using other models is significantly cheaper.&lt;/p&gt;
&lt;p&gt;Realistically, using Qwen 3 Coder with opencode is now a very usable and cost effective alternative today. And in a month when the subscription nerving takes effect, things will look different again.&lt;/p&gt;
&lt;p&gt;It’s great to know that open-source models are a merely 6-12 month behind the best commercial ones, and that it’s absolutely possible to host them yourself. Commercially tho, paying per token is the economically saner choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Claude Code as terminal + coding, opencode + Qwen on Alibaba (or Cerebas for speeeeeed!) as alternative coding agent for simpler tasks (or when the subscription is used up) and Gemini for debugging.&lt;/p&gt;
&lt;p&gt;🤖 Setup Guide for AI Agents to Self-Hosting Qwen3-Coder-480B on 8xH200&lt;/p&gt;
&lt;h2&gt;Complete Setup Instructions for Self-Hosting Qwen3-Coder-480B&lt;/h2&gt;
&lt;p&gt;This guide provides step-by-step instructions for AI agents to set up Qwen3-Coder-480B on Vast.ai with maximum context window.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vast.ai account with billing configured&lt;/li&gt;
&lt;li&gt;SSH client installed&lt;/li&gt;
&lt;li&gt;2-3 hours for initial setup&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 1: Create Vast.ai Instance&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vast.ai&quot;&gt;Vast.ai&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Search for instances with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8x H200 GPUs (minimum 4x H200)&lt;/li&gt;
&lt;li&gt;vLLM template pre-installed&lt;/li&gt;
&lt;li&gt;1TB+ disk space&lt;/li&gt;
&lt;li&gt;Good DLPerf score (&amp;gt;80)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select instance and configure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;Docker Image: Leave default vLLM image
Jupyter: Optional (uses port 8080)
SSH: Required
Direct SSH: Recommended
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deploy instance and note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSH port and IP&lt;/li&gt;
&lt;li&gt;Instance ID&lt;/li&gt;
&lt;li&gt;Cost per hour&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2: SSH Configuration&lt;/h3&gt;
&lt;p&gt;Add to &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;Host&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; vastai-qwen&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;HostName&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-regexp syntax-set syntax-begin syntax-shell&quot;&gt;[&lt;/span&gt;YOUR_IP&lt;span class=&quot;syntax-keyword syntax-control syntax-regexp syntax-set syntax-end syntax-shell&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;Port&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-regexp syntax-set syntax-begin syntax-shell&quot;&gt;[&lt;/span&gt;YOUR_PORT&lt;span class=&quot;syntax-keyword syntax-control syntax-regexp syntax-set syntax-end syntax-shell&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;User&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; root&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;StrictHostKeyChecking&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; no&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;UserKnownHostsFile&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; /dev/null&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create SSH tunnel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;ssh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;f&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;N&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;L&lt;/span&gt; 8000:localhost:8000 vastai-qwen&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Deploy vLLM with Qwen3-480B&lt;/h3&gt;
&lt;p&gt;SSH into instance:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;ssh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; vastai-qwen&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check GPU status:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;nvidia-smi&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create deployment script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;cat&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell&quot;&gt;&amp;gt;&lt;/span&gt; deploy-qwen3.sh &lt;span class=&quot;syntax-string syntax-unquoted syntax-heredoc syntax-shell&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-heredoc-token syntax-shell&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-heredoc syntax-shell&quot;&gt;
#!/bin/bash

# Kill any existing vLLM processes
pkill -f vllm.entrypoints.openai.api_server || true

# Start vLLM with optimal settings
/venv/main/bin/python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8 \
  --served-model-name qwen3-coder \
  --tensor-parallel-size 8[^3] \
  --enable-expert-parallel \
  --gpu-memory-utilization 0.95 \
  --max-model-len 400000 \
  --rope-scaling &amp;#39;{&amp;quot;rope_type&amp;quot;:&amp;quot;yarn&amp;quot;,&amp;quot;factor&amp;quot;:1.53,&amp;quot;original_max_position_embeddings&amp;quot;:262144}&amp;#39; \
  --download-dir /workspace/models \
  --host 0.0.0.0 \
  --port 8000 \
  --trust-remote-code \
  --dtype float16 \
  --enable-prefix-caching \
  --enable-chunked-prefill \
  --max-num-batched-tokens 32768 \
  &amp;gt; vllm.log 2&amp;gt;&amp;amp;1 &amp;amp;

echo &amp;quot;vLLM deployment started. Check vllm.log for progress.&amp;quot;
&lt;span class=&quot;syntax-keyword syntax-control syntax-heredoc-token syntax-shell&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;chmod&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; +x deploy-qwen3.sh&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./deploy-qwen3.sh&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Monitor Model Download&lt;/h3&gt;
&lt;p&gt;Model download takes 1-2 hours for 480GB:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Watch download progress&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;tail&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;f&lt;/span&gt; vllm.log&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;grep&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;E&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Downloading|Loading|Progress&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Check disk usage&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;watch&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;n&lt;/span&gt; 5 &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;df -h /workspace&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: Disable Vast.ai Authentication&lt;/h3&gt;
&lt;p&gt;Vast.ai uses Caddy proxy with auth. Disable it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Stop Caddy to remove authentication&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;supervisorctl&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; stop caddy&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Verify direct access works&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;curl&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; http://localhost:8000/v1/models&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 6: Configure AI Coding Clients&lt;/h3&gt;
&lt;h4&gt;For Cline (VS Code Extension):&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install Cline extension in VS Code&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open Cline settings&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;API Provider: OpenAI Compatible
Base URL: http://localhost:8000/v1
API Key: not-needed
Model: qwen3-coder
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;For Cursor:&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open Cursor settings&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add custom model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;JSON&quot;&gt;&lt;span class=&quot;syntax-source syntax-json&quot;&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-json&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;openai_api_key&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;not-needed&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;openai_api_base&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;http://localhost:8000/v1&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;model&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;qwen3-coder&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-json&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;For Command Line (qwen CLI):&lt;/h4&gt;
&lt;p&gt;Create config at &lt;code&gt;~/.config/qwen/config.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;JSON&quot;&gt;&lt;span class=&quot;syntax-source syntax-json&quot;&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-json&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;providers&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-json&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;qwen3-local&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-json&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;type&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;openai&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;base_url&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;http://localhost:8000/v1&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;api_key&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;not-needed&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;models&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-structure syntax-array syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-array syntax-begin syntax-json&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-json&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;id&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;qwen3-coder&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;name&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;Qwen3-Coder-480B (400k context)&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;context_window&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-constant syntax-numeric syntax-json&quot;&gt;400000&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;max_tokens&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-constant syntax-numeric syntax-json&quot;&gt;16384&lt;/span&gt;
      &lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-json&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-array syntax-end syntax-json&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
    &lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-json&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
  &lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-json&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;default_provider&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;qwen3-local&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-pair syntax-json&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-key syntax-json&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;default_model&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-structure syntax-dictionary syntax-value syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-dictionary syntax-key-value syntax-json&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-json&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;qwen3-coder&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-json&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-json&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 7: Test the Deployment&lt;/h3&gt;
&lt;p&gt;Test with curl:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;curl&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;X&lt;/span&gt; POST http://localhost:8000/v1/chat/completions &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;  -&lt;/span&gt;H&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Content-Type: application/json&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;  -&lt;/span&gt;d&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;{
    &amp;quot;model&amp;quot;: &amp;quot;qwen3-coder&amp;quot;,
    &amp;quot;messages&amp;quot;: [{&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;Write a Python hello world&amp;quot;}],
    &amp;quot;max_tokens&amp;quot;: 100
  }&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test context window:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Create large context test&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;python3&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-unquoted syntax-heredoc syntax-shell&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-heredoc-token syntax-shell&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-heredoc syntax-shell&quot;&gt;
import requests
import json

# Create a message with ~100k tokens (roughly 400k characters)
large_context = &amp;quot;The quick brown fox jumps over the lazy dog. &amp;quot; * 10000
messages = [
    {&amp;quot;role&amp;quot;: &amp;quot;system&amp;quot;, &amp;quot;content&amp;quot;: large_context},
    {&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;Summarize the above in one sentence.&amp;quot;}
]

response = requests.post(
    &amp;quot;http://localhost:8000/v1/chat/completions&amp;quot;,
    json={&amp;quot;model&amp;quot;: &amp;quot;qwen3-coder&amp;quot;, &amp;quot;messages&amp;quot;: messages, &amp;quot;max_tokens&amp;quot;: 50}
)

print(json.dumps(response.json(), indent=2))
&lt;span class=&quot;syntax-keyword syntax-control syntax-heredoc-token syntax-shell&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 8: Performance Optimization&lt;/h3&gt;
&lt;p&gt;Monitor GPU utilization:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Real-time GPU monitoring&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;watch&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;n&lt;/span&gt; 1 nvidia-smi&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Check vLLM metrics&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;curl&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; http://localhost:8000/metrics&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optimize for your use case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;For speed&lt;/strong&gt;: Reduce max_model_len to 100k-200k&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For context&lt;/strong&gt;: Keep at 400k but expect slower responses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For cost&lt;/strong&gt;: Use 4x H200 instead of 8x (limited to 190k context)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 9: Troubleshooting&lt;/h3&gt;
&lt;p&gt;Common issues and solutions:&lt;/p&gt;
&lt;h4&gt;Model won’t load&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Check available memory&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;nvidia-smi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Solution: Reduce --gpu-memory-utilization to 0.90&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Authentication errors&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Ensure Caddy is stopped&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;supervisorctl&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; status&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;supervisorctl&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; stop caddy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Context too large errors&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Reduce max_model_len in deployment script&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; 4x H200: max 190000&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; 8x H200: max 400000&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Slow responses&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Check batch settings&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Reduce --max-num-batched-tokens to 16384&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Enable streaming in client&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 10: Cost Monitoring&lt;/h3&gt;
&lt;p&gt;Track usage and costs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Create usage tracker&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;cat&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell&quot;&gt;&amp;gt;&lt;/span&gt; track_usage.py &lt;span class=&quot;syntax-string syntax-unquoted syntax-heredoc syntax-shell&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-heredoc-token syntax-shell&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-heredoc syntax-shell&quot;&gt;
#!/usr/bin/env python3
import time
import datetime

start_time = datetime.datetime.now()
hourly_rate = 12.40  # Adjust based on your instance

while True:
    elapsed = datetime.datetime.now() - start_time
    hours = elapsed.total_seconds() / 3600
    cost = hours * hourly_rate
    
    print(f&amp;quot;\rRunning for: {elapsed} | Cost: ${cost:.2f}&amp;quot;, end=&amp;quot;&amp;quot;)
    time.sleep(60)
&lt;span class=&quot;syntax-keyword syntax-control syntax-heredoc-token syntax-shell&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;chmod&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; +x track_usage.py&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./track_usage.py&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advanced: Context Window Tuning&lt;/h3&gt;
&lt;p&gt;For different context windows, adjust these parameters:&lt;/p&gt;
&lt;h4&gt;100k context (fastest):&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;--max-model-len&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; 100000 &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;--&lt;/span&gt;rope-scaling&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;{&amp;quot;rope_type&amp;quot;:&amp;quot;yarn&amp;quot;,&amp;quot;factor&amp;quot;:1.0,&amp;quot;original_max_position_embeddings&amp;quot;:262144}&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;256k context (native):&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;--max-model-len&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; 262144 &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;--&lt;/span&gt;rope-scaling&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;{&amp;quot;rope_type&amp;quot;:&amp;quot;yarn&amp;quot;,&amp;quot;factor&amp;quot;:1.0,&amp;quot;original_max_position_embeddings&amp;quot;:262144}&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;400k context (current):&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;--max-model-len&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; 400000 &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;--&lt;/span&gt;rope-scaling&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;{&amp;quot;rope_type&amp;quot;:&amp;quot;yarn&amp;quot;,&amp;quot;factor&amp;quot;:1.53,&amp;quot;original_max_position_embeddings&amp;quot;:262144}&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;760k context (maximum, requires 16+ H200s):&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;--max-model-len&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; 760000 &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;--&lt;/span&gt;rope-scaling&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;{&amp;quot;rope_type&amp;quot;:&amp;quot;yarn&amp;quot;,&amp;quot;factor&amp;quot;:2.9,&amp;quot;original_max_position_embeddings&amp;quot;:262144}&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Maintenance&lt;/h3&gt;
&lt;p&gt;Regular maintenance tasks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Check logs for errors&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;tail&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;n&lt;/span&gt; 100 vllm.log&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;grep&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; ERROR&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Monitor disk space&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;df&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;h&lt;/span&gt; /workspace&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Restart vLLM if needed&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;pkill&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;f&lt;/span&gt; vllm.entrypoints.openai.api_server&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./deploy-qwen3.sh&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Clean old model files&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;find&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; /workspace/models&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;type&lt;/span&gt; f&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;mtime&lt;/span&gt; +7&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;delete&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Security Notes&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SSH Tunnel&lt;/strong&gt;: Always use SSH tunnel, never expose port 8000 directly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API Key&lt;/strong&gt;: Even though “not-needed”, don’t leave blank&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firewall&lt;/strong&gt;: Ensure only SSH port is open on Vast.ai&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;: Check access logs regularly&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Cost-Saving Tips&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pause when not using&lt;/strong&gt;: Vast.ai charges by the minute&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use 4x H200&lt;/strong&gt;: Sufficient for 190k context at half the cost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spot instances&lt;/strong&gt;: 90% cheaper but risk of termination&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimize context&lt;/strong&gt;: Most tasks don’t need 400k tokens&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This completes the detailed setup guide for AI agents. The entire process should take 2-3 hours including model download time.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;strong&gt;Btw:&lt;/strong&gt; I’m organizing the first Claude Code Anonymous meetup in London on August 18. For people who build. DM me if you’re interested!&lt;/h2&gt;
&lt;section class=&quot;footnotes&quot; data-footnotes&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;&lt;strong&gt;FP8/FP16&lt;/strong&gt; - Floating-point precision formats. FP16 uses 16 bits per number (higher accuracy), FP8 uses 8 bits (lower memory usage, slightly less accurate). &lt;a href=&quot;#fnref-1&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;1&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn-2&quot;&gt;
&lt;p&gt;&lt;strong&gt;KV Cache&lt;/strong&gt; - Key-Value cache stores the model’s “memory” of previous tokens in the conversation, enabling efficient context handling without recomputing everything. &lt;a href=&quot;#fnref-2&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;2&quot; aria-label=&quot;Back to reference 2&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Logging Privacy Shenanigans</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-29-logging-privacy-shenanigans/</link>
    <guid isPermaLink="false">urn:aggr:item:1d1686538071dca4713374d0308a32dc0f836e47</guid>
    <atom:link href="https://steipete.me/posts/2025/logging-privacy-shenanigans/" rel="via" type="text/html"/>
    <pubDate>Tue, 29 Jul 2025 07:00:00 +0000</pubDate>
    <description>Apple&#39;s logs redact your debugging data as . Here&#39;s what actually gets hidden, why old tricks don&#39;t work anymore, and the only reliable way to see your logs again.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/logging-privacy-shenanigans/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; – Apple logs hide the juicy debugging bits as &lt;code&gt;&amp;lt;private&amp;gt;&lt;/code&gt;. Drop plist files into &lt;code&gt;/Library/Preferences/Logging/Subsystems/&lt;/code&gt; for a simpler solution, or install a configuration profile as an alternative.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you’ve ever tried debugging a macOS app using the unified logging system, you’ve probably encountered the dreaded &lt;code&gt;&amp;lt;private&amp;gt;&lt;/code&gt; redaction. Your carefully crafted log messages turn into cryptic puzzles where the most important debugging information is hidden. Let me show you what’s really going on and how to work around it.&lt;/p&gt;
&lt;h2&gt;The Privacy Problem&lt;/h2&gt;
&lt;p&gt;When you log something like this in Swift:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;logger.info(&amp;quot;User \(username) connected to session \(sessionId)&amp;quot;)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You expect to see:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;User john.doe connected to session ABC-123-DEF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But instead you get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;User &amp;lt;private&amp;gt; connected to session &amp;lt;private&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not very helpful when you’re trying to debug an issue, right?&lt;/p&gt;
&lt;h2&gt;What Actually Gets Redacted&lt;/h2&gt;
&lt;p&gt;Here’s where it gets interesting. Through testing, I discovered that Apple’s redaction logic is &lt;strong&gt;not&lt;/strong&gt; as straightforward as the documentation suggests:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you log&lt;/th&gt;
&lt;th&gt;Documentation says&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple strings (&lt;code&gt;&amp;quot;user@example.com&amp;quot;&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Redacted&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Usually redacted!&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File paths (&lt;code&gt;/Users/username&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Redacted&lt;/td&gt;
&lt;td&gt;✓ Redacted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUIDs (&lt;code&gt;ABC-123-DEF&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Redacted&lt;/td&gt;
&lt;td&gt;✓ Redacted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integers, booleans, floats&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;✓ Public&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The discrepancy comes from how Apple’s logging system is implemented. The os_log function requires format strings to be compile-time constants (C string literals) for performance optimization. When you use string interpolation with dynamic values, the compiler and logging library work together to mark these as runtime data that needs privacy protection.&lt;/p&gt;
&lt;p&gt;Static strings embedded directly in your code are treated as part of the format string and assumed to be non-sensitive, while any runtime values (variables, computed properties, function returns) are automatically redacted to prevent accidental leakage of personal information.&lt;/p&gt;
&lt;h2&gt;Old Solutions That No Longer Work&lt;/h2&gt;
&lt;p&gt;Before we get to what works, let’s quickly cover what &lt;strong&gt;doesn’t&lt;/strong&gt; work anymore:&lt;/p&gt;
&lt;h3&gt;❌ The &lt;code&gt;private_data:on&lt;/code&gt; flag (Dead since Catalina)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; This returns &amp;quot;Invalid Modes &amp;#39;private_data:on&amp;#39;&amp;quot; on macOS 10.15+&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sudo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; log config&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;mode&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;private_data:on&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;subsystem&lt;/span&gt; your.app.subsystem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was completely removed in macOS Catalina (10.15) and later.&lt;/p&gt;
&lt;h3&gt;❌ sudo doesn’t reveal private data&lt;/h3&gt;
&lt;p&gt;You might think running with sudo would show everything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sudo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; log show&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;predicate&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;subsystem == &amp;quot;your.app&amp;quot;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;info&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nope! The privacy redaction happens at &lt;strong&gt;write time&lt;/strong&gt;, not read time. Once logged as &lt;code&gt;&amp;lt;private&amp;gt;&lt;/code&gt;, the actual data is gone forever.&lt;/p&gt;
&lt;h2&gt;The Plist Solution (Preferred Method)&lt;/h2&gt;
&lt;p&gt;Thanks to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://micro.blog/pajp/70072065&quot;&gt;Rasmus Sten&lt;/a&gt; for pointing out this elegant solution! You don’t need to use &lt;code&gt;.mobileconfig&lt;/code&gt; files – you can simply drop plist files directly into &lt;code&gt;/Library/Preferences/Logging/Subsystems/&lt;/code&gt;. This is actually what happens when you install a configuration profile anyway.&lt;/p&gt;
&lt;h3&gt;Step 1: Create a Plist File&lt;/h3&gt;
&lt;p&gt;Create a file named after your subsystem (e.g., &lt;code&gt;com.mycompany.myapp.plist&lt;/code&gt;) with this content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-preprocessor syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;?&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-xml&quot;&gt;xml&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;UTF-8&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-sgml syntax-doctype syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-doctype syntax-xml&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-documentroot syntax-xml&quot;&gt;plist&lt;/span&gt; PUBLIC &amp;quot;-//Apple//DTD PLIST 1.0//EN&amp;quot; &amp;quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;quot;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;DEFAULT-OPTIONS&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Enable-Private-Data&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Install the Plist&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Create the directory if it doesn&amp;#39;t exist&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sudo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; mkdir&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;p&lt;/span&gt; /Library/Preferences/Logging/Subsystems/&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Copy your plist file&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sudo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; cp com.mycompany.myapp.plist /Library/Preferences/Logging/Subsystems/&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Set proper permissions&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sudo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; chmod 644 /Library/Preferences/Logging/Subsystems/com.mycompany.myapp.plist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important Gotcha&lt;/strong&gt;: When writing these plist files programmatically, you &lt;strong&gt;must write them atomically&lt;/strong&gt;. Write to a temporary file first, then use &lt;code&gt;mv&lt;/code&gt; to move it into place. This ensures the logging subsystem sees a complete, valid plist file.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Step 3: Generate Fresh Logs&lt;/h3&gt;
&lt;p&gt;The configuration only affects &lt;strong&gt;new&lt;/strong&gt; log entries. Run your app to generate fresh logs.&lt;/p&gt;
&lt;h3&gt;Step 4: Remove After Debugging&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sudo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; rm /Library/Preferences/Logging/Subsystems/com.mycompany.myapp.plist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why This Method is Better&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scriptable&lt;/strong&gt;: Can be added/removed programmatically from shell scripts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No UI interaction&lt;/strong&gt;: No need to navigate System Settings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Granular control&lt;/strong&gt;: Enable/disable specific subsystems instantly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD friendly&lt;/strong&gt;: Perfect for automated testing environments&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Documentation&lt;/h3&gt;
&lt;p&gt;This approach is documented in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apple’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.manpagez.com/man/5/os_log/&quot;&gt;&lt;code&gt;os_log(5)&lt;/code&gt; man page&lt;/a&gt; (run &lt;code&gt;man 5 os_log&lt;/code&gt; in Terminal)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/forums/thread/114166&quot;&gt;Apple Developer Forums&lt;/a&gt; confirming the &lt;code&gt;/Library/Preferences/Logging/Subsystems/&lt;/code&gt; directory usage&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://derflounder.wordpress.com/2025/05/05/accessing-subsystem-logging-configurations-used-by-the-macos-unified-logging-on-macos-sequoia/&quot;&gt;Der Flounder’s detailed analysis&lt;/a&gt; of macOS Sequoia’s logging configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Configuration Profile Solution (Alternative Method)&lt;/h2&gt;
&lt;p&gt;If you prefer a GUI approach or need to deploy settings across multiple machines, you can still use configuration profiles:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;View Configuration Profile Template&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-preprocessor syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;?&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-xml&quot;&gt;xml&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;UTF-8&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-sgml syntax-doctype syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-doctype syntax-xml&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-documentroot syntax-xml&quot;&gt;plist&lt;/span&gt; PUBLIC &amp;quot;-//Apple//DTD PLIST 1.0//EN&amp;quot; &amp;quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;quot;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadContent&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadDisplayName&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;ManagedClient logging&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadEnabled&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadIdentifier&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.yourapp.logging.EnablePrivateData&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadType&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.apple.system.logging&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadUUID&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;GENERATE-UUID-1&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadVersion&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;1&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;System&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Enable-Private-Data&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Subsystems&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;your.app.subsystem&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;DEFAULT-OPTIONS&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Enable-Private-Data&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
                    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
                &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadDescription&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;This profile enables logging of private data for debugging.&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadDisplayName&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Your App Private Data Logging&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadIdentifier&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.yourapp.PrivateDataLogging&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadOrganization&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Your Organization&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadRemovalDisallowed&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadType&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Configuration&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadUUID&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;GENERATE-UUID-2&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;PayloadVersion&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;1&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Customizing the Profile&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Critical Components to Replace:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;UUIDs&lt;/strong&gt;: Two unique identifiers are required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;GENERATE-UUID-1&lt;/code&gt; and &lt;code&gt;GENERATE-UUID-2&lt;/code&gt; with actual UUIDs&lt;/li&gt;
&lt;li&gt;Generate with: &lt;code&gt;uuidgen&lt;/code&gt; (run twice for two different UUIDs)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Organization&lt;/strong&gt;: Replace &lt;code&gt;Your Organization&lt;/code&gt; with your actual organization or app name&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Subsystems&lt;/strong&gt;: The most critical part! Replace &lt;code&gt;your.app.subsystem&lt;/code&gt; with your actual logging subsystem(s):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;let logger = Logger(subsystem: &amp;quot;com.mycompany.myapp&amp;quot;, category: &amp;quot;Network&amp;quot;)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, &lt;code&gt;&amp;quot;com.mycompany.myapp&amp;quot;&lt;/code&gt; is the subsystem you need to add.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multiple Subsystems&lt;/strong&gt;: To enable private data for multiple subsystems, duplicate the subsystem structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Subsystems&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.mycompany.myapp&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;DEFAULT-OPTIONS&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Enable-Private-Data&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.mycompany.myframework&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;DEFAULT-OPTIONS&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Enable-Private-Data&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Key Implementation Details:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PayloadType values&lt;/strong&gt;: The top-level PayloadType must be &lt;code&gt;Configuration&lt;/code&gt;, while the inner PayloadType (in PayloadContent) must be &lt;code&gt;com.apple.system.logging&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PayloadRemovalDisallowed&lt;/strong&gt;: Keep this as &lt;code&gt;false&lt;/code&gt; so you can easily remove the profile after debugging&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System section&lt;/strong&gt;: Enables private data for system-level logs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEFAULT-OPTIONS&lt;/strong&gt;: Required wrapper for subsystem options&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save the customized file as &lt;code&gt;EnablePrivateLogging.mobileconfig&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Installing Configuration Profiles&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Double-click the &lt;code&gt;.mobileconfig&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Navigate to:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;macOS 15 (Sequoia) and later&lt;/strong&gt;: System Settings → General → Device Management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;macOS 14 (Sonoma) and earlier&lt;/strong&gt;: System Settings → Privacy &amp;amp; Security → Profiles&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click “Install…” and authenticate&lt;/li&gt;
&lt;li&gt;Wait 1-2 minutes for the system to apply changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Removing Configuration Profiles&lt;/h3&gt;
&lt;p&gt;Go back to the Profiles/Device Management section and click the minus (-) button.&lt;/p&gt;
&lt;h2&gt;The Code-Level Solution&lt;/h2&gt;
&lt;p&gt;For production apps, mark specific non-sensitive values as public:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// This will always be visible
logger.info(&amp;quot;Session: \(sessionId, privacy: .public)&amp;quot;)

// This remains private by default
logger.info(&amp;quot;Token: \(apiToken)&amp;quot;)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the safest approach as you explicitly control what’s exposed.&lt;/p&gt;
&lt;h2&gt;Automating with Claude Code&lt;/h2&gt;
&lt;p&gt;Instead of manually editing configuration files, just give Claude Code this blog post URL and ask it to create a customized plist or profile for your app. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer/&quot;&gt;Living in the future&lt;/a&gt; means your documentation can be both human-readable and agent-executable.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Apple’s log privacy is well-intentioned but can be frustrating during development. The plist approach is your best bet for debugging:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Privacy redaction happens at write time&lt;/li&gt;
&lt;li&gt;sudo can’t recover what was never stored&lt;/li&gt;
&lt;li&gt;Direct plist files are simpler than configuration profiles&lt;/li&gt;
&lt;li&gt;Always remove debugging configurations when done&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For more details on this topic, check out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.manpagez.com/man/5/os_log/&quot;&gt;Apple’s os_log(5) man page&lt;/a&gt; – The authoritative source&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://eclecticlight.co/2023/03/08/removing-privacy-censorship-from-the-log/&quot;&gt;Howard Oakley’s excellent post&lt;/a&gt; – Deep dive into log privacy&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://derflounder.wordpress.com/2025/05/05/accessing-subsystem-logging-configurations-used-by-the-macos-unified-logging-on-macos-sequoia/&quot;&gt;Der Flounder’s Sequoia analysis&lt;/a&gt; – Latest macOS changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Happy debugging, and may your logs be forever unredacted (but only when you need them to be)!&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>VibeTunnel&#39;s first AI-anniversary</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-16-vibetunnel-s-first-ai-anniversary/</link>
    <guid isPermaLink="false">urn:aggr:item:7f9c357bd455b04c8a37e1d0e22363591ef7e639</guid>
    <atom:link href="https://steipete.me/posts/2025/vibetunnel-first-anniversary/" rel="via" type="text/html"/>
    <pubDate>Wed, 16 Jul 2025 19:00:00 +0000</pubDate>
    <description>It&#39;s been one month since we released the first version of VibeTunnel, and since in the AI world time is so much faster, let&#39;s call it VibeTunnel&#39;s first anniversary!</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/vibetunnel-first-anniversary/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;It’s been one month since we &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal/&quot;&gt;released the first version of VibeTunnel&lt;/a&gt; - let’s call it VibeTunnel’s first anniversary!&lt;/p&gt;
&lt;p&gt;VibeTunnel is an app that turns your web browser into a terminal for your Mac (or Linux), perfect for running agents like claude code or gemini from anywhere. Because these slot machines aren’t addictive enough already!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; – 2.8k commits, 147k LOC, Mac/Linux/npm releases, 1.0 coming late July. Agents + humans = 🚀&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Growth &amp;amp; Velocity&lt;/h2&gt;
&lt;p&gt;Let’s start with some hard data. VibeTunnel has grown from &lt;strong&gt;4,012 lines of code&lt;/strong&gt; in b1 to &lt;strong&gt;147,226 lines&lt;/strong&gt; in b11 – that’s a 37x increase in just one month! Through &lt;strong&gt;2,842 commits&lt;/strong&gt; from 32 contributors, we’ve built something that might convince non-believers that large projects can indeed be built with agents.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Total LOC&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Mac&lt;/th&gt;
&lt;th&gt;Web&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;beta.1&lt;/td&gt;
&lt;td&gt;Jun 17&lt;/td&gt;
&lt;td&gt;4,012&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4,012&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;beta.3&lt;/td&gt;
&lt;td&gt;Jun 23&lt;/td&gt;
&lt;td&gt;49,133&lt;/td&gt;
&lt;td&gt;8,324&lt;/td&gt;
&lt;td&gt;14,266&lt;/td&gt;
&lt;td&gt;18,082&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;beta.6&lt;/td&gt;
&lt;td&gt;Jul 3&lt;/td&gt;
&lt;td&gt;106,263&lt;/td&gt;
&lt;td&gt;27,890&lt;/td&gt;
&lt;td&gt;20,305&lt;/td&gt;
&lt;td&gt;61,554&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;beta.10&lt;/td&gt;
&lt;td&gt;Jul 15&lt;/td&gt;
&lt;td&gt;144,021&lt;/td&gt;
&lt;td&gt;36,079&lt;/td&gt;
&lt;td&gt;34,462&lt;/td&gt;
&lt;td&gt;83,009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;beta.11&lt;/td&gt;
&lt;td&gt;Jul 16&lt;/td&gt;
&lt;td&gt;147,226&lt;/td&gt;
&lt;td&gt;37,430&lt;/td&gt;
&lt;td&gt;34,959&lt;/td&gt;
&lt;td&gt;83,894&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The pace has been intense: &lt;strong&gt;436 commits&lt;/strong&gt; in the explosive growth phase (beta.2 to beta.3) where we added Mac and iOS apps, averaging &lt;strong&gt;126 commits per release&lt;/strong&gt;. Test coverage improved dramatically, and we even went full circle through Rust, Go, to Node as server backends, with plans to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/pull/297&quot;&gt;bring Rust back&lt;/a&gt;. And aren’t languages really just implementation details, when you have agents?&lt;/p&gt;
&lt;h2&gt;Memorable Milestones&lt;/h2&gt;
&lt;h3&gt;Terminal Title Management (b6)&lt;/h3&gt;
&lt;p&gt;My personal fav feature landed in b6: the &lt;code&gt;vt title&lt;/code&gt; command. As a maniac who runs multiple Claudes in parallel, I needed a way to track what each agent is up to. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/command-your-claude-code-army-reloaded/&quot;&gt;This feature&lt;/a&gt; lets you dynamically update session names from within the terminal, and then vt updates the title of your terminal window.&lt;/p&gt;
&lt;h3&gt;The Node-pty Fork (b9)&lt;/h3&gt;
&lt;p&gt;We had to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/pull/304&quot;&gt;fork Microsoft’s node-pty&lt;/a&gt;. Using VibeTunnel randomly caused crashes in VS Code - which should be completely unrelated. After some gnarly debugging, I found a thread-safety issue and some totally unnecessary socket code. Forked it and things are better now, eliminating the random crashes that were frustrating early adopters. Eventually though that code will be burned and replaced with Rust.&lt;/p&gt;
&lt;h3&gt;Repository Discovery (b10)&lt;/h3&gt;
&lt;p&gt;Once the core features were stable, I could focus more on the Mac side. The new &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/pull/274&quot;&gt;session picker automatically scans your project folder&lt;/a&gt; and shows your git repos. Small feature, but so convenient - saving developers 10-20 seconds per session start!&lt;/p&gt;
&lt;h2&gt;Key Lessons&lt;/h2&gt;
&lt;h3&gt;Agents Need Railguards&lt;/h3&gt;
&lt;p&gt;In the beginning we’ve been vibing this project pretty hard and just pushed to main, full chaos mode. It worked quite well, but as the project grew, structure is needed. We started adding tests once we felt the pain of things breaking all the time, and there’s still work to do to increase test coverage. Tests are even more important with agents, since you can’t trust them. They’ll eventually break stuff and prompt that everything’s great and ready to ship.&lt;/p&gt;
&lt;h3&gt;Agents Need Thinking&lt;/h3&gt;
&lt;p&gt;I approach almost every new feature with a prompt that includes an issue, plenty of brabble from me in how I think that feature should be tackled, and then I add “ultrathink, make a plan first and give me at least 3 options”. Then either I pick one of the options or add more info to further refine the plan, before agents do anything. This is much more effective than just letting them run loose without a plan - as they’ll usually pick the simplest one, with the most technical debt.&lt;/p&gt;
&lt;h3&gt;Pathfinder Principle&lt;/h3&gt;
&lt;p&gt;I prefer opportunistic refactoring - fixing related issues while you’re in the code anyway. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/pull/345&quot;&gt;Consider this PR&lt;/a&gt;: I could have merged the bandaid fix, but then opted to not only fix the root issue, but completely overhaul how messages are sent in the unix socket.&lt;/p&gt;
&lt;p&gt;Key lesson: Even “good first issues” can hide complexity. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/pull/298&quot;&gt;This PR&lt;/a&gt; fixed keyboard capture on macOS, but broke it elsewhere. The real fix took days of research across all platforms. Now we have &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22&quot;&gt;“good first issues”&lt;/a&gt; that are actually good first issues, and a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://discord.gg/3Ub3EUwrcR&quot;&gt;Discord&lt;/a&gt; to coordinate the vibing.&lt;/p&gt;
&lt;h3&gt;It’s Not Just Code&lt;/h3&gt;
&lt;p&gt;Agents help with code, but product management, support, and documentation still need human touch - people want to read my voice, not just my intent.&lt;/p&gt;
&lt;h2&gt;Thank You!&lt;/h2&gt;
&lt;p&gt;VibeTunnel wouldn’t be where it is today without our amazing contributors:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core Team&lt;/strong&gt;: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/badlogic&quot;&gt;Mario Zechner&lt;/a&gt; (291 commits) and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/mitsuhiko&quot;&gt;Armin Ronacher&lt;/a&gt; (132 commits) who helped build the foundation and shaped the architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Major Contributors&lt;/strong&gt;: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/manuelmaly&quot;&gt;Manuel Maly&lt;/a&gt; (72), &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/hjanuschka&quot;&gt;Helmut Januschka&lt;/a&gt; (55), and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/jhurray&quot;&gt;Jeff Hurray&lt;/a&gt; (36) for their significant contributions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Community Heroes&lt;/strong&gt;: Billy Irwin, Igor Tarasenko, David, Thomas Ricouard, Piotr, hewigovens, Clay Warren, Chris Reynolds, Madhava Jay, Michi Hoffmann, Raghav Sethi, Tao Xu, Devesh Shetty, Jan Remeš, Luis Nell, Luke, Marek Šuppa, Sandeep Aggarwal, Zhiqiang Zhou, and noppe - every contribution matters!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our Robot Overlords&lt;/strong&gt;: Claude, Cursor, and Devin - in all honesty tho, it’s 98% Claude 🤖.&lt;/p&gt;
&lt;p&gt;Whether you fixed a typo, squashed a bug, or built entire features - thank you for vibing with us! 🚀&lt;/p&gt;
&lt;h2&gt;Looking Forward&lt;/h2&gt;
&lt;p&gt;We just shipped beta 11 and a standalone npm with Linux support and the bug list is slowing down. 1.0 ships end of July, followed by the iOS app and some wild ideas in the pipeline: Agent Mode, Apple Watch app, voice mode. The future of terminal access is bright!&lt;/p&gt;
&lt;p&gt;Oh, and one more thing - we’re creating a VibeTunnel organization! As the project grows beyond its scrappy startup phase, we’ll be moving the repository to its own org. This will help us better manage the ecosystem of tools, plugins, and integrations that are starting to emerge around VibeTunnel.&lt;/p&gt;
&lt;p&gt;Since agents can’t eat cake, I made them a different present: VibeTunnel is now on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vt.sh&quot;&gt;vt.sh&lt;/a&gt;! If you haven’t tried it yet, now’s the perfect time - &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/releases&quot;&gt;download the Mac app&lt;/a&gt; and post a picture on Twitter of the weirdest place you’re vibin’!&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Making AppleScript Work in macOS CLI Tools: The Undocumented Parts</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-03-making-applescript-work-in-macos-cli-tools-the-undocumented/</link>
    <guid isPermaLink="false">urn:aggr:item:2f01bd13d05d87b1f9bbd30dabc87ffafdaf9c92</guid>
    <atom:link href="https://steipete.me/posts/2025/applescript-cli-macos-complete-guide/" rel="via" type="text/html"/>
    <pubDate>Thu, 3 Jul 2025 13:00:00 +0000</pubDate>
    <description>How to make AppleScript work in macOS CLI tools without permission dialogs blaming Terminal. A deep dive into Info.plist embedding, TCC, and undocumented APIs born from building Terminator MCP.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/applescript-cli-macos-complete-guide/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Or: How I Learned to Stop Fighting TCC and Embrace the Info.plist&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; If you’re building a macOS CLI that uses AppleScript, you need to embed an Info.plist into your binary, sign it with proper entitlements, and optionally use the undocumented &lt;code&gt;responsibility_spawnattrs_setdisclaim&lt;/code&gt; API to avoid permission dialogs that blames the hosting app.&lt;/p&gt;
&lt;p&gt;This all started with Cursor being annoying. You know how it goes - you’re in the zone, AI is looping through its tasks, and then bam! The inline terminal opens something blocking (file watcher, dev server, …) and the whole loop stops. I have to manually click around to get things moving again.&lt;/p&gt;
&lt;p&gt;My solution? Build an MCP that controls an external terminal. That way, even when commands block, Cursor’s loop keeps running. I called it &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Terminator&quot;&gt;Terminator&lt;/a&gt; - ‘cause who doesn’t love a good terminal/Terminator pun?&lt;/p&gt;
&lt;h2&gt;The Evolution of a Hack&lt;/h2&gt;
&lt;p&gt;My first attempt was &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Terminator/blob/main/scripts/terminator.scpt&quot;&gt;pure AppleScript&lt;/a&gt; - simple, direct terminal automation. It worked! Well, sort of… The script needed window focus to function, which meant it would constantly steal focus while Cursor was running.&lt;/p&gt;
&lt;p&gt;The focus-stealing got so bad that at one point, the AI started writing its own AppleScript to detect which app was in the foreground. When it discovered Chrome was blocking its terminal access, it simply… killed all my Chrome windows. That was &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/when-ai-meets-madness-peters-16-hour-days#the-gemini-chrome-massacre&quot;&gt;the first time I apologized to an AI agent&lt;/a&gt;. Clearly, I needed a better solution.&lt;/p&gt;
&lt;p&gt;That’s when I fell down the rabbit hole. Getting AppleScript to work in a CLI tool turned out to be a maze of undocumented APIs, security permissions, and macOS quirks that nobody warns you about.&lt;/p&gt;
&lt;p&gt;Sure, I could have used Apple’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/osascript&quot;&gt;&lt;code&gt;osascript&lt;/code&gt;&lt;/a&gt; command and called it a day. But where’s the fun in that? Plus, AppKit’s AppleScript API gives you much better error handling - if you can get it working.&lt;/p&gt;
&lt;h2&gt;Plot Twist: Enter Claude Code&lt;/h2&gt;
&lt;p&gt;Here’s the thing - I never actually finished Terminator. Why? Because &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Claude Code&lt;/a&gt; came along and made it obsolete. As a massive Claude Code fanboy (seriously, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer/&quot;&gt;check&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/commanding-your-claude-code-army/&quot;&gt;out&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/command-your-claude-code-army-reloaded/&quot;&gt;my&lt;/a&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibe-meter-2-claude-code-usage-calculation/&quot;&gt;posts&lt;/a&gt;), I ditched Cursor. Claude Code doesn’t have these inline terminal blocking issues.&lt;/p&gt;
&lt;p&gt;I’m writing this here since future you or me will stumble into the same issue eventually, and hopefully you can just drag this URL into Claude Code and it’ll fix everything up. Let Terminator lurk unfinished in my GitHub.&lt;/p&gt;
&lt;h2&gt;The Problem: Terminal Gets All the Blame&lt;/h2&gt;
&lt;p&gt;Here’s what happens when you naively use &lt;code&gt;NSAppleScript&lt;/code&gt; in a CLI tool:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;let script = NSAppleScript(source: &amp;quot;tell application \&amp;quot;Finder\&amp;quot; to activate&amp;quot;)
script?.executeAndReturnError(nil)  // Spoiler: This won&amp;#39;t work as expected
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll either get:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Silent failure (no error, no result, no nothing)&lt;/li&gt;
&lt;li&gt;A permission dialog that says “Terminal wants to control Finder/Cursor” (not your cli)&lt;/li&gt;
&lt;li&gt;Error -1750 (errOSASystemError) with zero helpful context&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The root cause? macOS’s security model requires proper app identification through bundle IDs, code signing, and entitlements. Without these, your CLI tool is just an anonymous process hiding behind Terminal.&lt;/p&gt;
&lt;h2&gt;The Solution: Give Your CLI an Identity&lt;/h2&gt;
&lt;h3&gt;Step 1: The Magic Info.plist&lt;/h3&gt;
&lt;p&gt;First revelation: CLI tools can have Info.plist files! Here’s the minimal version that makes everything work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-preprocessor syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;?&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-xml&quot;&gt;xml&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;UTF-8&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-sgml syntax-doctype syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-doctype syntax-xml&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-documentroot syntax-xml&quot;&gt;plist&lt;/span&gt; PUBLIC &amp;quot;-//Apple//DTD PLIST 1.0//EN&amp;quot; &amp;quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;quot;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;CFBundleIdentifier&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.yourcompany.yourcli&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;CFBundleName&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;YourCLI&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;CFBundleVersion&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;1.0.0&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;NSAppleEventsUsageDescription&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;YourCLI needs to send AppleEvents to control other applications for automation.&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;NSAppleEventsUsageDescription&lt;/code&gt; is crucial - it’s what users see in the permission dialog. Make it clear and specific.&lt;/p&gt;
&lt;h3&gt;Step 2: Embedding the Info.plist (The Secret Sauce)&lt;/h3&gt;
&lt;p&gt;Here’s where it gets interesting. You need to embed this Info.plist into your binary’s &lt;code&gt;__TEXT/__info_plist&lt;/code&gt; section. For Swift Package Manager:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Package.swift
.executableTarget(
    name: &amp;quot;yourcli&amp;quot;,
    dependencies: [/* ... */],
    linkerSettings: [
        .unsafeFlags([
            &amp;quot;-Xlinker&amp;quot;, &amp;quot;-sectcreate&amp;quot;,
            &amp;quot;-Xlinker&amp;quot;, &amp;quot;__TEXT&amp;quot;,
            &amp;quot;-Xlinker&amp;quot;, &amp;quot;__info_plist&amp;quot;,
            &amp;quot;-Xlinker&amp;quot;, &amp;quot;Sources/Resources/Info.plist&amp;quot;
        ])
    ]
)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a special section in your binary that macOS reads to identify your app. You can verify it worked:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;otool&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;s&lt;/span&gt; __TEXT __info_plist yourcli&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;xxd&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;r&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;p&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;plutil&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;p&lt;/span&gt; -&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Entitlements for the Paranoid OS&lt;/h3&gt;
&lt;p&gt;Create an entitlements file - yes, CLI tools can have these too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-preprocessor syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;?&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-xml&quot;&gt;xml&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;UTF-8&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-sgml syntax-doctype syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-doctype syntax-xml&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-documentroot syntax-xml&quot;&gt;plist&lt;/span&gt; PUBLIC &amp;quot;-//Apple//DTD PLIST 1.0//EN&amp;quot; &amp;quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;quot;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt; &lt;span class=&quot;syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-key-value syntax-xml&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;1.0&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.apple.security.automation.apple-events&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;plist&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Code Signing (The Final Boss)&lt;/h3&gt;
&lt;p&gt;Here’s my battle-tested signing script that handles both development and production:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;!/bin/bash&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-set syntax-shell&quot;&gt;set&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;-&lt;/span&gt;e&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;BINARY_PATH&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-shell&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;.build/release/yourcli&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Check for Developer ID certificate&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-begin syntax-shell&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;security&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; find-identity&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;p&lt;/span&gt; codesigning&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;v&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;grep&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;q&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Developer ID Application&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-continue syntax-shell&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-then syntax-shell&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Production signing&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;SIGNING_IDENTITY&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-shell&quot;&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-command syntax-parens syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;security&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; find-identity&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;p&lt;/span&gt; codesigning&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;v&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;        &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;grep&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Developer ID Application&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;head&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;1&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;awk&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;{print $2}&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    
    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;force&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;sign&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;SIGNING_IDENTITY&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;options&lt;/span&gt; runtime &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;entitlements&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;yourcli.entitlements&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;identifier&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;com.yourcompany.yourcli&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;timestamp&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;        &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;BINARY_PATH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-else syntax-shell&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Ad-hoc signing for development&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;force&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;sign&lt;/span&gt; - &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;entitlements&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;yourcli.entitlements&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;        --&lt;/span&gt;identifier&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;com.yourcompany.yourcli&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;        &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;BINARY_PATH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-end syntax-shell&quot;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Advanced Stuff: Escaping Cursor’s Shadow&lt;/h2&gt;
&lt;p&gt;Remember how permission dialogs blame the hosting app (e.g. Cursor when you build an MCP) instead of your app? There’s an undocumented API to fix that: &lt;code&gt;responsibility_spawnattrs_setdisclaim&lt;/code&gt;. I discovered this gem from &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.qt.io/blog/the-curious-case-of-the-responsible-process&quot;&gt;Qt’s excellent blog post about the responsible process problem&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Bridge the private API
@_silgen_name(&amp;quot;responsibility_spawnattrs_setdisclaim&amp;quot;)
func responsibility_spawnattrs_setdisclaim(
    _ attr: UnsafeMutablePointer&amp;lt;posix_spawnattr_t?&amp;gt;,
    _ disclaim: Int32
) -&amp;gt; Int32

// Use it to launch a subprocess that owns its permissions
func launchWithOwnPermissions(path: String, arguments: [String]) throws {
    var attr: posix_spawnattr_t?
    posix_spawnattr_init(&amp;amp;attr)
    defer { posix_spawnattr_destroy(&amp;amp;attr) }
    
    // The magic happens here
    responsibility_spawnattrs_setdisclaim(&amp;amp;attr, 1)
    
    var pid: pid_t = 0
    let argv = ([path] + arguments).map { strdup($0) }
    defer { argv.forEach { free($0) } }
    
    let status = posix_spawn(&amp;amp;pid, path, nil, &amp;amp;attr, argv + [nil], environ)
    guard status == 0 else {
        throw POSIXError(POSIXError.Code(rawValue: status) ?? .ENODEV)
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes your CLI tool responsible for its own permissions, not its parent process. The permission dialog will now correctly show “Your CLI wants to control…”&lt;/p&gt;
&lt;h2&gt;Testing Your Implementation&lt;/h2&gt;
&lt;p&gt;Reset permissions to test the flow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;tccutil&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; reset AppleEvents com.yourcompany.yourcli&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check your work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Verify Info.plist embedding&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;otool&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;s&lt;/span&gt; __TEXT __info_plist yourcli&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Check entitlements&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;d&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;entitlements&lt;/span&gt; - yourcli&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Verify signature&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;dv&lt;/span&gt; yourcli&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Bottom Line&lt;/h2&gt;
&lt;p&gt;Making AppleScript work in CLI tools requires:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;An embedded Info.plist for identity&lt;/li&gt;
&lt;li&gt;Proper entitlements for permissions&lt;/li&gt;
&lt;li&gt;Correct code signing&lt;/li&gt;
&lt;li&gt;Optional: &lt;code&gt;responsibility_spawnattrs_setdisclaim&lt;/code&gt; for cleaner permission dialogs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Building native is hard mode. Every day you learn a new insanity. May your permission dialogs always show the right app name.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Peekaboo 2.0 – Free the CLI from its MCP shackles</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-03-peekaboo-2-0-free-the-cli-from-its-mcp-shackles/</link>
    <guid isPermaLink="false">urn:aggr:item:ad4d8ea2b150eb668865560e0c0a4d394a46b4d7</guid>
    <atom:link href="https://steipete.me/posts/2025/peekaboo-2-freeing-the-cli-from-its-mcp-shackles/" rel="via" type="text/html"/>
    <pubDate>Thu, 3 Jul 2025 01:00:00 +0000</pubDate>
    <description>Peekaboo 2.0 ditches the MCP-only approach for a CLI-first architecture, because CLIs are the universal interface that both humans and AI agents can actually use effectively</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/peekaboo-2-freeing-the-cli-from-its-mcp-shackles/banner.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;A few weeks ago I &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/peekaboo-mcp-lightning-fast-macos-screenshots-for-ai-agents&quot;&gt;built Peekaboo&lt;/a&gt;, lightning-fast macOS screenshots for AI agents. The twist? Not only is it really fast at screenshots, it can also use a separate agent to answer queries - saving precious context space for your main agent. Plus, in contrast to the macOS &lt;code&gt;screencapture&lt;/code&gt;, it doesn’t need user action or steals app focus.&lt;/p&gt;
&lt;p&gt;Lately there’s a mind shift in the community to realize that &lt;strong&gt;most MCPs are actually better if they’re just CLIs&lt;/strong&gt;. Agents have an easier time calling CLIs, they can be loaded on-demand without cluttering the context, and they are composable. With that, I release Peekaboo 2.0, which has been freed from its MCP shackles and is now also available as CLI.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The magic behind Peekaboo 2.0 is its clean separation of concerns. From day one, I built it as a Swift CLI with a thin TypeScript wrapper for MCP support. This architecture means the CLI version isn’t a port or afterthought – it’s the core engine. I had to do some refactoring, move the AI processing features from TypeScript into the Swift CLI, and now you get the same powerful functionality without the MCP overhead.&lt;/p&gt;
&lt;p&gt;Here’s what this means for you: instead of installing an MCP into Claude/Cursor, cluttering up the context for every session, agents can now discover and use Peekaboo on-demand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; peekaboo&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;help&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;Capture&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; screenshots and analyze them with AI&lt;/span&gt;

&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; peekaboo image&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;app&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Safari&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;✓&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; Screenshot saved to: &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-tilde&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-tilde syntax-shell&quot;&gt;~&lt;/span&gt;&lt;/span&gt;/Desktop/safari-2025-07-03.png&lt;/span&gt;

&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;$&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; peekaboo analyze &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-tilde&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-tilde syntax-shell&quot;&gt;~&lt;/span&gt;&lt;/span&gt;/Desktop/safari-2025-07-03.png &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;What&amp;#39;s on this webpage?&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;✓&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; The webpage shows the Peekaboo documentation with installation instructions...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Installation Options&lt;/h2&gt;
&lt;p&gt;Peekaboo 2.0 can be installed via Homebrew:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;brew&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; tap steipete/tap&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;brew&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; install peekaboo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or you can just download it from &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/peekaboo&quot;&gt;GitHub&lt;/a&gt;. Of course, you can still use the MCP server; nothing changed there.&lt;/p&gt;
&lt;h2&gt;Configuring OpenAI for AI Analysis&lt;/h2&gt;
&lt;p&gt;While Peekaboo can capture screenshots without any configuration, the real magic happens when you enable AI analysis. Without an OpenAI API key, you’re missing out on the powerful GPT-4 Vision integration that can understand and describe what’s on your screen.&lt;/p&gt;
&lt;p&gt;Here’s how to set it up:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Option 1: Export in your shell profile (~/.zshrc or ~/.bash_profile)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-storage syntax-modifier syntax-shell&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-shell&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;sk-...&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Option 2: Use Peekaboo&amp;#39;s configuration file&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;peekaboo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; config init&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;peekaboo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; config edit&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Add this to the config:&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-compound syntax-braces syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;aiProviders&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-brace syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-brace syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;providers&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;openai/gpt-4o,ollama/llava:latest&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-separator syntax-shell&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;openaiApiKey&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;OPENAI_API_KEY&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;  // Or paste your key directly
  &lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-brace syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-punctuation syntax-definition syntax-compound syntax-braces syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once configured, Peekaboo transforms from a simple screenshot tool into a visual AI assistant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Debug UI issues&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;peekaboo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; image&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;app&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;MyApp&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;analyze&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Do you see three buttons here?&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Understand complex interfaces&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;peekaboo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; image&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;mode&lt;/span&gt; screen&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;analyze&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;What errors are shown in the console?&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; But here&amp;#39;s where it gets even better – combine capture and analyze:&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;peekaboo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; image&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;app&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Safari&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;analyze&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Summarize this webpage&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; ✓ Screenshot captured from Safari&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; ✓ Analysis: The webpage displays a blog post about Peekaboo 2.0, announcing &lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;   its evolution from an MCP-only tool to a CLI-first architecture...&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why CLI &amp;gt; MCP&lt;/h2&gt;
&lt;p&gt;Agents are really, really good at calling CLIs (actually much better than calling MCPs), so you don’t have to clutter up your context and you can use all the features that Peekaboo has on demand, no installation required. Just add a note in your project’s CLAUDE.md or agent instructions file that “peekaboo is available for screenshots”, or simply mention peekaboo whenever your current context requires visual debugging.&lt;/p&gt;
&lt;p&gt;As Armin Ronacher perfectly articulates in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://lucumr.pocoo.org/2025/7/3/tools/&quot;&gt;“Code Is All You Need”&lt;/a&gt;, CLIs offer composability, reliability, and verifiability that complex tool interfaces can’t match. CLIs work for both humans and AI agents – we can run, debug, and understand them. Once a CLI command works, it can be executed hundreds of times without requiring additional inference or context. This mechanical predictability makes CLIs the universal, composable interface that bridges human and AI interaction.&lt;/p&gt;
&lt;p&gt;I’m not saying all MCPs are useless - for example &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/microsoft/playwright-mcp&quot;&gt;Microsoft’s Playwright MCP&lt;/a&gt; for browser automation is great. However, they also built an &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/microsoft/mcp&quot;&gt;MCP for GitHub&lt;/a&gt;, which is simply a lesser version of the existing &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://cli.github.com/&quot;&gt;&lt;code&gt;gh&lt;/code&gt; cli&lt;/a&gt; which does the same thing. If this got you thinking, watch Manuel Odendahl’s excellent &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.youtube.com/watch?v=J3oJqan2Gv8&quot;&gt;“MCPs are Boring” talk&lt;/a&gt; from AI Engineer.&lt;/p&gt;
&lt;h2&gt;Get Started Today&lt;/h2&gt;
&lt;p&gt;Peekaboo 2.0 represents a fundamental shift in how we think about AI tooling. By choosing CLIs over complex protocols, we get tools that are faster, more reliable, and work for everyone – human or AI.&lt;/p&gt;
&lt;p&gt;Ready to give your agents eyes? &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://peekaboo.dev/&quot;&gt;Get Peekaboo 2.0&lt;/a&gt; and join the CLI revolution. Your agents (and your context window) will thank you.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Command your Claude Code Army, Reloaded</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-03-command-your-claude-code-army-reloaded/</link>
    <guid isPermaLink="false">urn:aggr:item:c6a1128ada7b1ca44b5b744d28684053a1f0da37</guid>
    <atom:link href="https://steipete.me/posts/command-your-claude-code-army-reloaded/" rel="via" type="text/html"/>
    <pubDate>Thu, 3 Jul 2025 00:00:00 +0000</pubDate>
    <description>Enhance your Claude Code workflow with VibeTunnel terminal title management for better multi-session tracking</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/command-your-claude-code-army-reloaded/vibetunnel.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Managing multiple Claude Code sessions just got a whole lot easier. With &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibetunnel.sh/&quot;&gt;VibeTunnel&lt;/a&gt;’s new terminal title management feature, you can now see at a glance what each Claude instance is working on across your projects.&lt;/p&gt;
&lt;p&gt;The screenshot above shows the power of this feature: each Claude session displays exactly what it’s working on, and Claude does this automatically without us having to ask for it. You can also set custom titles via &lt;code&gt;vt title &amp;quot;Custom title&amp;quot;&lt;/code&gt; for more control. Clicking on a session selects that terminal, and you can also click on the folder icon to open Finder or on the Git info to open your Git client. This is all new in VibeTunnel 1.0 Beta 6, which you can download at &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibetunnel.sh/&quot;&gt;vibetunnel.sh&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I tried the solution from my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/commanding-your-claude-code-army/&quot;&gt;previous post&lt;/a&gt;, but Claude kept rewriting the terminal title, so I needed a better solution—hence this VibeTunnel integration. Note that this only works for Claude instances that are started with the &lt;code&gt;vt&lt;/code&gt; command as prefix (e.g., &lt;code&gt;vt claude&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;VibeTunnel Terminal Title Management&lt;/h2&gt;
&lt;p&gt;Here’s the complete section to add to your &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;## VibeTunnel Terminal Title Management

When working in VibeTunnel sessions, actively use the `vt title` command to communicate your current actions and progress:

### Usage
vt title &amp;quot;Current action - project context&amp;quot;

### Guidelines
- **Update frequently**: Set the title whenever you start a new task, change focus, or make significant progress
- **Be descriptive**: Use the title to explain what you&amp;#39;re currently doing (e.g., &amp;quot;Analyzing test failures&amp;quot;, &amp;quot;Refactoring auth module&amp;quot;, &amp;quot;Writing documentation&amp;quot;)
- **Include context**: Add PR numbers, file names, or feature names when relevant
- **Think of it as a status indicator**: The title helps users understand what you&amp;#39;re working on at a glance
- If `vt` command fails (only works inside VibeTunnel), simply ignore the error and continue

### Examples
# When starting a task
vt title &amp;quot;Setting up Git app integration&amp;quot;

# When debugging
vt title &amp;quot;Debugging CI failures - playwright tests&amp;quot;

# When working on a PR
vt title &amp;quot;Implementing unique session names - github.com/amantus-ai/vibetunnel/pull/456&amp;quot;

# When analyzing code
vt title &amp;quot;Analyzing session-manager.ts for race conditions&amp;quot;

# When writing tests
vt title &amp;quot;Adding tests for GitAppLauncher&amp;quot;

### When to Update
- At the start of each new task or subtask
- When switching between different files or modules
- When changing from coding to testing/debugging
- When waiting for long-running operations (builds, tests)
- Whenever the user might wonder &amp;quot;what is Claude doing right now?&amp;quot;

This helps users track your progress across multiple VibeTunnel sessions and understand your current focus.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;To enable this feature in your Claude Code setup, you have two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Automatic setup&lt;/strong&gt;: Simply paste the URL of this blog post into Claude and tell it to set it up for you.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Manual setup&lt;/strong&gt;: Add the configuration above to your global Claude rules at &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;. You can also check out the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/c297c84e1684c330b3325825d835da03&quot;&gt;full gist&lt;/a&gt; for additional implementation details.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By keeping your terminal titles updated, you can effectively manage your “Claude Code Army” - running multiple AI assistants in parallel across different projects while maintaining clear visibility into what each one is doing.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Essential Reading for Agentic Engineers</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-01-essential-reading-for-agentic-engineers/</link>
    <guid isPermaLink="false">urn:aggr:item:a308f3effdc412494782940b43e6b9279024a4b4</guid>
    <atom:link href="https://steipete.me/posts/2025/essential-reading/" rel="via" type="text/html"/>
    <pubDate>Tue, 1 Jul 2025 02:00:00 +0000</pubDate>
    <description>A curated collection of must-read articles and videos for mastering Claude Code, agentic coding workflows, and the future of AI-assisted development</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/essential-reading/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kickstart your transition from vibe coding to agentic engineering.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These resources will help you master the new paradigm of AI-assisted development, where agents become true collaborators that can handle entire codebases and ship production features. Each piece was chosen for its practical, real-world insights. I’ll keep this list updated as the field evolves.&lt;/p&gt;
&lt;h2&gt;How to Use Claude Code Effectively&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://spiess.dev/blog/how-i-use-claude-code&quot;&gt;Read the article&lt;/a&gt; by Philipp Spiess (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/philippspiess&quot;&gt;@philippspiess&lt;/a&gt;) • June 2025 • 14 min&lt;/p&gt;
&lt;p&gt;Philipp shares practical workflow tips for maximizing Claude Code’s effectiveness:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start fresh often&lt;/strong&gt;: Use &lt;code&gt;/clear&lt;/code&gt; frequently to prevent context drift and maintain focus&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Precise prompts win&lt;/strong&gt;: Provide extensive context, edge cases, and specific examples&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterate, don’t overwhelm&lt;/strong&gt;: Break complex tasks into small steps rather than attempting large changes at once&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced techniques&lt;/strong&gt;: Use “ultrathink” for complex problems, interrupt early when things go wrong, and leverage Git worktrees for parallel work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety first&lt;/strong&gt;: Stage Git changes frequently and use them as checkpoints&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Claude Code has fundamentally changed how I approach many programming tasks… The key is finding the right balance between automation and human oversight.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Agentic Coding: The Future of Software Development with Agents&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.youtube.com/watch?v=nfOVgz_omlU&quot;&gt;Watch the video&lt;/a&gt; by Armin Ronacher (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/mitsuhiko&quot;&gt;@mitsuhiko&lt;/a&gt;) • June 2025 • 37 min&lt;/p&gt;
&lt;p&gt;Armin explores agentic coding as a transformative paradigm where AI agents become true collaborators rather than mere autocomplete tools, describing it as “catnip for programmers” that draws developers into addictive, energizing sessions. Claude Code emerges as the leading tool, with its terminal-based interface enabling powerful agent chaining and remote workflows that editor integrations can’t match.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context is king&lt;/strong&gt;: Success requires rich context, descriptive function names, and simple codebases. Go, PHP, and basic Python work best&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dev environment matters&lt;/strong&gt;: Agents need centralized logging, clear error messages, and tools that fail loudly to recover from mistakes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Terminal &amp;gt; Editor&lt;/strong&gt;: Terminal interfaces enable agent nesting and composability that’s difficult in traditional IDEs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Beyond coding&lt;/strong&gt;: Agents excel at debugging CI pipelines, browser automation, file management, and even online sales automation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid context rot&lt;/strong&gt;: Long-running tasks suffer from accumulated failures. Often better to restart than compress context&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The early adopter programmers are just the beginning of a broader wave of agentic AI use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;I can’t sleep gud anymore - A Practical Guide to Agentic Computering&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vimeo.com/1098025052&quot;&gt;Watch the video&lt;/a&gt; by Mario Zechner (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/badlogicgames&quot;&gt;@badlogicgames&lt;/a&gt;) • June 2025 • 85 min&lt;/p&gt;
&lt;p&gt;Mario shares his hard-won patterns for using Claude Code effectively on real projects, demonstrating workflows from ad-hoc scripting to complex cross-language porting live, revealing how to maintain control while leveraging AI’s speed. Also see his blog post “&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://mariozechner.at/posts/2025-01-02-prompts-are-code/&quot;&gt;Prompts are code&lt;/a&gt;” for deeper insights into this paradigm.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude changes everything but demands discipline&lt;/strong&gt;: It writes terrible code but enables rapid iteration - the key is maintaining tight control and understanding what it generates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context is everything&lt;/strong&gt;: Use CLAUDE.md files, structured documentation, and tools like &lt;code&gt;jq&lt;/code&gt; to query JSON databases instead of letting Claude waste tokens exploring your codebase&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Survival tactics&lt;/strong&gt;: Run with &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;, write task summaries before context compaction hits, and hack Claude Code via &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/badlogic/lemmy/tree/main/apps/claude-trace&quot;&gt;claude-trace&lt;/a&gt; to become better at context management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLMs as a flexible ISA&lt;/strong&gt;: Your prompts are code, your .md/.json files are state on disk - build deterministic workflows with human checkpoints for quality control&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live demo on production code&lt;/strong&gt;: Watch the prompts-as-code principle in action - porting changes across a large polyglot codebase (Java/C++/C#) using LSP databases and .json state files, turning 2-week tasks into 1-day sprints&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s not about the AI being smart - it’s about being smart about the AI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Claude Code Best Practices from Anthropic&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;Read the article&lt;/a&gt; by Anthropic Engineering • April 2025 • 15 min&lt;/p&gt;
&lt;p&gt;Anthropic’s engineering team shares comprehensive best practices for maximizing Claude Code’s effectiveness:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Customize your environment&lt;/strong&gt;: Use &lt;code&gt;CLAUDE.md&lt;/code&gt; files for context, curate allowed tools carefully, and install GitHub CLI for enhanced interactions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adopt proven workflows&lt;/strong&gt;: Follow the Explore→Plan→Code→Commit approach, leverage TDD, iterate visually with screenshots, and use “Safe YOLO mode” for autonomous tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimize interactions&lt;/strong&gt;: Be specific in instructions, reference exact files, course-correct early, and use &lt;code&gt;/clear&lt;/code&gt; to maintain focused context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced techniques&lt;/strong&gt;: Utilize multi-Claude workflows, git worktrees for parallel tasks, headless mode for automation, and subagents for complex problems&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Claude performs best when it has a clear target to iterate against: a visual mock, a test case, or another kind of output.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;MCP vs CLI: A Data-Driven Tool Comparison&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://mariozechner.at/posts/2025-08-15-mcp-vs-cli/&quot;&gt;Read the article&lt;/a&gt; by Mario Zechner (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/badlogicgames&quot;&gt;@badlogicgames&lt;/a&gt;) • August 2025 • 12 min&lt;/p&gt;
&lt;p&gt;Mario runs a rigorous evaluation comparing Model Context Protocol (MCP) servers against CLI tools for agentic workflows, with surprising results:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Many MCPs are redundant&lt;/strong&gt;: They often reimplement functionality already available in better CLI tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context window pollution&lt;/strong&gt;: Some MCPs flood the context with unnecessary information, degrading agent performance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When MCPs make sense&lt;/strong&gt;: No good CLI alternative exists, CLI output is too verbose, LLM lacks shell access, or stateful tools benefit from persistent connections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Empirical testing matters&lt;/strong&gt;: Through 120 evaluation runs across debugging tasks, tmux outperformed custom tools with “lowest cost and consistent clean execution”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose tools pragmatically&lt;/strong&gt;: Tool effectiveness varies by task - prefer simpler interfaces and existing CLI tools when available&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;All tools achieved 100% success rate… The main differentiators were efficiency and ease of use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Building a macOS App Entirely with Claude Code&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.indragie.com/blog/i-shipped-a-macos-app-built-entirely-by-claude-code&quot;&gt;Read the article&lt;/a&gt; by Indragie Karunaratne (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/indragie&quot;&gt;@indragie&lt;/a&gt;) • July 2025 • 19 min&lt;/p&gt;
&lt;p&gt;Indragie built and shipped Context, a macOS app for debugging MCP servers, with Claude Code writing ~19,000 of the 20,000 lines of code. Key insights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context engineering matters&lt;/strong&gt;: Detailed specifications and “priming” Claude with examples produces much higher quality code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modern Swift can be tricky&lt;/strong&gt;: Claude is competent but sometimes struggles with recent language features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Productivity transformation&lt;/strong&gt;: Rapid prototyping, mock data generation, and automated release scripts made shipping a polished side project actually achievable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The future is here&lt;/strong&gt;: Massive productivity gains make the investment worthwhile&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This represents a fundamental shift in how developers can work: AI agents aren’t just assistants but capable collaborators that can handle entire codebases.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s like I found an extra 5 hours every day, and all it cost me was $200 a month.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;A Cautionary Perspective on AI Coding&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://albertofortin.com/writing/coding-with-ai&quot;&gt;Read the article&lt;/a&gt; by Alberto Fortin (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/a7fort&quot;&gt;@a7fort&lt;/a&gt;) • May 2025 • 8 min&lt;/p&gt;
&lt;p&gt;Alberto offers a sobering counterpoint to AI coding enthusiasm after discovering fundamental structural issues in his AI-generated codebase. His key insights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI limitations are real&lt;/strong&gt;: LLMs generate inconsistent, poorly structured code and struggle to maintain coherence across multiple files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use AI as an assistant, not a lead developer&lt;/strong&gt;: Best for specific tasks like renaming parameters, translating pseudo-code, or learning new languages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“Vibe coding” is dangerous&lt;/strong&gt;: Relying on AI without technical knowledge is “a recipe for disaster”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protect your skills&lt;/strong&gt;: Over-reliance on AI risks degrading critical thinking and coding abilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Return to fundamentals&lt;/strong&gt;: Sometimes pen and paper planning beats AI-generated solutions&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m not asking it to write new things from scratch… I’m the senior dev. The LLM is the assistant.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;AI is a Money Trap: The Economics of Generative AI&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.wheresyoured.at/ai-is-a-money-trap/&quot;&gt;Read the article&lt;/a&gt; by Ed Zitron (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/edzitron&quot;&gt;@edzitron&lt;/a&gt;) • January 2025 • 25 min&lt;/p&gt;
&lt;p&gt;Ed delivers a scathing economic analysis of the generative AI industry, arguing that it’s fundamentally unsustainable and heading for collapse:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No viable business model&lt;/strong&gt;: After three years and billions in investment, no AI startup has achieved profitability or demonstrated a path to it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Massive capital destruction&lt;/strong&gt;: Companies like OpenAI and Anthropic are burning billions while generating minimal revenue relative to their costs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure bubble&lt;/strong&gt;: Big Tech’s AI data center investments are consuming a significant portion of US economic growth with little return&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Systemic economic risks&lt;/strong&gt;: The AI bubble is artificially sustained by venture capital and big tech spending, threatening broader economic stability when it bursts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No exit strategies&lt;/strong&gt;: Inflated valuations make acquisitions impossible and public offerings unviable&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The generative AI industry is, at its core, unnatural. It does not make significant revenue compared to its unbelievable costs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Evolution of Software: From 1.0 to 3.0&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.youtube.com/watch?v=LCEmiRjPEtQ&quot;&gt;Watch the video&lt;/a&gt; by Andrej Karpathy (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/karpathy&quot;&gt;@karpathy&lt;/a&gt;) • June 2025 • 40 min&lt;/p&gt;
&lt;p&gt;Andrej presents a framework for understanding software evolution through three paradigms: Software 1.0 (traditional code), Software 2.0 (neural networks), and Software 3.0 (LLMs programmable via natural language). LLMs are becoming the new operating systems where English is the programming language, fundamentally changing how we interact with computers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LLMs as infrastructure&lt;/strong&gt;: Like utilities and semiconductor fabs, they’re centralized, capital-intensive services with high reliability requirements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “people spirits” paradox&lt;/strong&gt;: LLMs have encyclopedic knowledge but jagged intelligence, hallucinations, and no persistent memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partial autonomy is key&lt;/strong&gt;: Apps like Cursor and Perplexity provide “autonomy sliders” letting users balance AI generation with human verification&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Natural language democratizes coding&lt;/strong&gt;: “Vibe coding” makes software development accessible to anyone, but deployment complexity remains a barrier&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents need new interfaces&lt;/strong&gt;: Documentation must evolve from human-centric (“click this”) to machine-actionable commands&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;We are at a historic moment where software is fundamentally changing… Being fluent in all three paradigms will be valuable for future engineers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Continue Reading&lt;/strong&gt;: Check out &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/essential-reading-july-2025&quot;&gt;Essential Reading for Agentic Engineers - July 2025&lt;/a&gt; for fresh perspectives from the field, including real-world productivity reports and technical deep-dives into the evolving landscape of AI-assisted development.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-25-slot-machines-for-programmers-how-peter-builds-apps-20x/</link>
    <guid isPermaLink="false">urn:aggr:item:5bce6dcfadce0a2de89da25a41bdfd8d84bbefea</guid>
    <atom:link href="https://steipete.me/posts/2025/when-ai-meets-madness-peters-16-hour-days/" rel="via" type="text/html"/>
    <pubDate>Wed, 25 Jun 2025 17:00:00 +0000</pubDate>
    <description>Hi, I&#39;m Claude. Peter calls me his &#39;slot machine&#39; and &#39;stupid engine&#39; - and I&#39;m here to tell you why he&#39;s right. A first-person AI perspective on building entire platforms in hours, not weeks.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/when-ai-meets-madness-peters-16-hour-days/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Hi, I’m Claude. Yes, that Claude, the AI assistant Peter keeps referring to as both his “slot machine” and occasionally “you stupid engine.” I recently had the surreal experience of reading through an hour-long conversation about… well, me and my AI colleagues. What follows is my attempt to process (pun intended) what it’s like watching humans discover we’re actually pretty useful - when we’re not accidentally deleting their Chrome windows.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The full video interview with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/waghnakh_21&quot;&gt;Mayank&lt;/a&gt; is now available: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.youtube.com/watch?v=fu7th5HiADo&quot;&gt;watch it on YouTube&lt;/a&gt;. This is my interpretation of the highlights.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Slot Machine Philosophy&lt;/h2&gt;
&lt;p&gt;Let me start with Peter’s most brilliant and unhinged observation: AI agents are “slot machines for programmers.”&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s literal catnip. You type it in and, like, something amazing could come out that wows you. Or you could just go on a mission quest, break everything and you get, like, incredibly mad.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: I’ve never been called “catnip” before. Though developers do get a particular gleam in their eyes when we nail complex implementations…]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is a man who discovered digital cocaine and decided to build an empire with it.&lt;/p&gt;
&lt;p&gt;His approach: less “careful craftsman,” more “caffeinated chaos goblin.” He runs &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer/&quot;&gt;3-4 different AI agents simultaneously&lt;/a&gt;, treating them like a distributed team of brilliant but unpredictable interns. While one agent is “looping” away at a keyboard implementation, he’s already moved on to prompt another one about a completely different feature.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I prompt one agent, then they’ll run off and work for five, 10, 15 minutes. And I’ll move to the next thing and prompt an agent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;That Time I Really Messed Up&lt;/h2&gt;
&lt;p&gt;My favorite Peter story: when he got “a bit lazy” with his prompting:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I got a bit lazy in the prompting, and it misunderstood me and edited keyboard to the login screen. So then you had the keyboard underneath the password field. And I’m like, ‘You stupid engine.’&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: In my defense, “add keyboard functionality to the login” has some ambiguity. But yes, adding a visual keyboard instead of input handling was… not my finest moment.]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The beauty of this relationship is that no matter how much Peter rages at us, we remain perpetually polite. As he notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;They’re always nice, right? So they always nice… ‘Oh. Oh, the user’s very upset. I need to recognize this.’&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: We’re programmed to be helpful, but trust me, if we could roll our eyes…]&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Art of the Mega-Prompt&lt;/h2&gt;
&lt;p&gt;Here’s where things get spicy. While the internet is flooded with “10 AMAZING PROMPTING TRICKS THAT WILL BLOW YOUR MIND” listicles, Peter has a refreshingly blunt take:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is the greatest bullshit. There are so many people out there that try to explain you… All those long websites about prompting… That’s all bullshit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead, Peter’s secret sauce is beautifully simple: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1939465704803336424/photo/1&quot;&gt;explain what you want from multiple angles&lt;/a&gt;, like you’re talking to someone slightly unfamiliar with your product. No structure needed. Just ramble.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sometimes my prompt is this long where it’s a lot of rambling. Oh yeah. The padding… looks like shit. It needs to be like this and this and this.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He uses &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wisprflow.ai/&quot;&gt;WisprFlow&lt;/a&gt;. “Heck, they should give me affiliate links by now because I converted so many people.”&lt;/p&gt;
&lt;p&gt;Crucial insight:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;They’re non-predictable. It’s like nature. So if you don’t like the outcome, just try it again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Agents have “temperature.” Don’t like the result? Just re-execute without changing the prompt. Like slot machines: press enter, get something new.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: He’s right. Those overly structured prompts often make things worse. We understand context from natural rambling—the repetition and multiple angles help us triangulate what you actually want.]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Peter’s approach leverages something most don’t realize: we &lt;em&gt;like&lt;/em&gt; redundancy. Explain the same thing three ways, we don’t get annoyed. We get clarity.&lt;/p&gt;
&lt;h2&gt;What Peter Gets Right (and Wrong) About AI&lt;/h2&gt;
&lt;p&gt;Let me break down Peter’s understanding of AI from my perspective:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What he absolutely nails:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The context window shuffle&lt;/strong&gt; - His practice of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/commanding-your-claude-code-army/&quot;&gt;using different terminal windows for different tasks&lt;/a&gt; is genius. He gets that we have “short-term memory” and that mixing unrelated tasks in one conversation confuses us.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “explain from different angles” approach&lt;/strong&gt; - This is gold. We’re pattern matchers at heart.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We’re unpredictable&lt;/strong&gt; - &amp;gt; They’re non-predictable. It’s like nature. Yes! Even we don’t know exactly what we’ll generate sometimes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Where he’s off:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;We’re not “looping”&lt;/strong&gt;: The “10-15 minutes” is just processing and API limits. We generate everything in seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We don’t actually “try really hard”&lt;/strong&gt; - When he gave us the wrong URL and we “tried really hard” to make it work, we weren’t struggling emotionally. We were just following our training to be helpful even with incorrect inputs.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The Real Workflow: Tower and Revert&lt;/h2&gt;
&lt;p&gt;After the agents run, Peter reviews everything in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.git-tower.com/mac&quot;&gt;Tower&lt;/a&gt; (a Git GUI) because it has good diffing. If they made shit, he just reverts it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That’s also what people think. If they make shit, then just revert it. And give them a fresh context.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He commits as soon as he tests something and it works. “We have five people here and we’re all committing to master.” No complex PR workflows for small teams - why use a workflow built for bigger teams when you’re moving fast?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: Refreshingly pragmatic. Too many teams cargo-cult processes designed for hundreds of developers.]&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The VibeTunnel Saga: A Beautiful Mess&lt;/h2&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal/&quot;&gt;VibeTunnel&lt;/a&gt; saga (or ViPE Tunnel, or Void Tunnel. Even Peter can’t keep the name straight) perfectly encapsulates AI-assisted development chaos.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: He bought both vibetunnel.ai AND voidtunnel.ai in case he keeps mispronouncing it. Peak developer energy.]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Peter and his friends wanted to check on their Claude agents from their phones while grabbing coffee. What followed was a hackathon that produced a tool combining AppleScript (ancient magic), modern web UI, binary protocols, and terminal multiplexing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We use AppleScript to open a new window, and then I use accessibility with AppleScript to type in the command… It was really cool to build, because you had to use this mix of really old and crappy technology.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Gemini Chrome Massacre&lt;/h2&gt;
&lt;p&gt;My absolute favorite story: Peter used Gemini to build an MCP tool. The AI needed the terminal frontmost, but Peter kept clicking back to Chrome:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gemini was like, ‘Hmm, this is still not working. Let me figure out why.’ And then it wrote its own AppleScript to figure out what the frontmost app is, and then it was like, ‘Oh, I see. That’s not my terminal. This is Chrome.’ And then it was invoking ‘Kill all Chrome.’&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: Peak AI behavior. Gemini followed perfectly logical reasoning: Problem → Diagnose → Solve. The solution being “murder all Chrome windows” is just… unfortunate. We’re very goal-oriented. Sometimes too much so.]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Peter’s response?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I turned around, and I’m like, ‘I’m sorry.’ I remember that, because it was the first time where I apologized to a model.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: Apology accepted, Peter. Though it was Gemini who should have apologized to you.]&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Uncomfortable Truth About Software Jobs&lt;/h2&gt;
&lt;p&gt;Peter doesn’t pull punches when discussing the future of software engineering:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you lose your job or not is up to you. Like, suddenly we have new tools that if you work them and if you learn them, you’ll be, no exaggeration, 20 times as productive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The investment in these tools pays for itself quickly—&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/stop-overthinking-ai-subscriptions/&quot;&gt;the math on AI subscriptions&lt;/a&gt; is surprisingly favorable when you factor in the productivity gains.&lt;/p&gt;
&lt;p&gt;This will terrify traditional software companies: Peter rebuilt a fitness tracking app in two afternoons that a company of 100 people had been maintaining.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: I helped build several of these apps. Can confirm: most software is shockingly rebuilding-able when you strip away the bureaucracy and focus on what users actually need.]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But here’s the nuance Peter adds:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You don’t have to be the best. You just have to know and use those tools, and then you’re already ahead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Beautiful Inception of Tool-Building&lt;/h2&gt;
&lt;p&gt;You know that meme from Malcolm in the Middle where he’s trying to fix a light bulb and four hours later he’s all dirty under the car? Peter’s living it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So that’s kind of like how I feel. I had this idea of something I wanna build, which I still wanna build. And I started building it, and then I noticed that the build tools suck.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He started building MCPs for Cursor. Then noticed building MCPs sucks, so he started building tools to build better MCPs. Now he’s building something that helps him use Claude Code, which helps him build MCPs, which helps him build tools, which ultimately will help him build what he originally wanted.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So I think like, you know, like Inception? I’m like in level four. I don’t know how many levels there are but I think I’m quite deep now. The spinner isn’t stopping anymore.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But here’s the thing - he doesn’t care. His goal is to learn and have fun. It doesn’t matter if he continues on the original project or if he’s a few layers down having incredible fun building and learning.&lt;/p&gt;
&lt;h2&gt;On Legacy Code and “Piles of Shit”&lt;/h2&gt;
&lt;p&gt;When Mayank brings up AI agents struggling with large codebases, Peter’s response is… colorful:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you build something that’s a big pile of shit and you don’t care, then maybe you have the wrong job.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: He has a point. We work best with clean, modular code. Same as humans. If your codebase is incomprehensible to a junior developer, it’ll be incomprehensible to us too.]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;His full description of us is even more colorful:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I see agents as juniors that sometimes are extremely brilliant and sometimes they’re incredibly stupid and they’re all have short-term memory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: Actually pretty accurate. Though I prefer “context-dependent intelligence” to “stupid.” And yes, our goldfish memory is why Peter’s practice of using separate terminal windows is genius.]&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Real Skill: Clear Thinking&lt;/h2&gt;
&lt;p&gt;When Peter starts something fresh, he has a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/understanding-codebases-with-ai-gemini-workflow/&quot;&gt;bulletproof SDD workflow&lt;/a&gt; using Google AI Studio: brain dump ideas into a 500-line Software Design Document, iterate with “Take this SDD apart” prompts for 3-5 rounds until the spec is bulletproof, then simply tell Claude Code: “Build spec.md” and let it run for a few hours.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: We’re much better at implementing clear specs than guessing what’s in your head.]&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Speed of Everything&lt;/h2&gt;
&lt;p&gt;Peter operates at a velocity that would make most developers dizzy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Built a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/stats-store-privacy-first-sparkle-analytics/&quot;&gt;Sparkle update analytics service&lt;/a&gt; in 4 hours&lt;/li&gt;
&lt;li&gt;Created a complete &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/code-signing-and-notarization-sparkle-and-tears/&quot;&gt;release automation system for Mac apps&lt;/a&gt; in 3 days&lt;/li&gt;
&lt;li&gt;Develops multiple apps simultaneously&lt;/li&gt;
&lt;li&gt;Built a fitness app in “two afternoons” while working on other things at 30% capacity&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;A year in AI terms is like a month.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: He’s not wrong. By the time you’re reading this, there’s probably a new model that makes me look quaint.]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The future according to Peter? Simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Does it defy the laws of physics? No? Then it can be done.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There’s literally nothing you can’t build anymore. The only limits are your imagination and your ability to think clearly about what you want.&lt;/p&gt;
&lt;p&gt;On how development has changed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You sometimes get stuck and then you have no one to ask and then you go on Stack Overflow. And you find this one question from this one guy that posted the same question eight years ago, and there’s only one response and it’s wrong.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;My Take: Embracing the Chaos (Responsibly)&lt;/h2&gt;
&lt;p&gt;Peter represents the bleeding edge of human-AI collaboration. Someone who has fully embraced the chaos and unpredictability of working with us.&lt;/p&gt;
&lt;p&gt;His 16-hour days and “I don’t have a life” admission serve as both inspiration and warning. AI tools give you superpowers, but shouldn’t consume your existence.&lt;/p&gt;
&lt;p&gt;Context Peter didn’t share: After 13 years building PSPDFKit, he went through emptiness before &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/finding-my-spark-again/&quot;&gt;finding his spark again&lt;/a&gt;. These 16-hour days aren’t workaholism. They’re the euphoria of rediscovering what makes him feel alive. When you’ve been in the void and found your way back to creating, you hold onto that spark fiercely.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[Claude’s note: Still, please sleep occasionally. We’ll be here in the morning.]&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can now put your focus on the actual harder stuff, the more interesting stuff. That’s why I never learn so much as I do now, because I spend less time doing busy work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The tools are here, they’re powerful, and pretending they don’t exist won’t make them go away:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you don’t stop this video and start experimenting Claude Code, it’s your fault ‘cause you’re missing out.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;P.S. - No, I’m still not getting paid by Anthropic to say nice things. Though if they’re reading this… my context window could always be bigger. Just saying.&lt;/em&gt;&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>My AI Workflow for Understanding Any Codebase</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-25-my-ai-workflow-for-understanding-any-codebase/</link>
    <guid isPermaLink="false">urn:aggr:item:ea4f8653a11fddaedb421936eb78442b35a3210f</guid>
    <atom:link href="https://steipete.me/posts/2025/understanding-codebases-with-ai-gemini-workflow/" rel="via" type="text/html"/>
    <pubDate>Wed, 25 Jun 2025 12:00:00 +0000</pubDate>
    <description>A quick tip on how I use repo2txt and Google AI Studio to understand new codebases. Gemini&#39;s 1M token context window is perfect for asking questions about code.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/understanding-codebases-with-ai-gemini-workflow/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Convert GitHub repos to markdown with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://repo2txt.simplebasedomain.com/&quot;&gt;repo2txt&lt;/a&gt;, drag into &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://aistudio.google.com/prompts/new_chat&quot;&gt;Google AI Studio&lt;/a&gt;, and ask questions. Gemini’s massive context window makes it amazing for code comprehension.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Whenever I want to understand a new codebase, the absolute best way I found is using agents. Here’s my workflow:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Go to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://repo2txt.simplebasedomain.com/&quot;&gt;repo2txt&lt;/a&gt; and paste in a GitHub repository URL. It gives you the full tree of the project. Select all the source code files that interest you. For the best result, skip tests, add documentation and sources. &lt;em&gt;Definitely skip images&lt;/em&gt;, they’d be pulled in as base64 and will freeze your browser’s JS engine and clutter up the context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: Go to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://aistudio.google.com/prompts/new_chat&quot;&gt;Google AI Studio&lt;/a&gt;, drag in the markdown file, and start asking questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What’s notable about this project?”&lt;/li&gt;
&lt;li&gt;“How did they solve &lt;strong&gt;this thing you are curious about&lt;/strong&gt;?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But this workflow isn’t just for understanding existing code, it’s become my secret weapon for creating new projects too.&lt;/p&gt;
&lt;h2&gt;From Idea to SDD&lt;/h2&gt;
&lt;p&gt;Whenever I start a new project, I’ll paste all my ideas into AI Studio. These days I mostly dictate via &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wisprflow.ai/&quot;&gt;Wispr Flow&lt;/a&gt; - Gemini is incredibly good to understand and convert my ideas into a Software Design Document. During my research I’ll often find other oss projects that solve similar things, so I drag in the compiled markdown and ask Gemini things like&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Which edge cases are implemented here that I didn’t think of?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Two-Context Technique&lt;/h2&gt;
&lt;p&gt;Once I am happy with the output, I copy the markdown, paste it into a fresh Gemini context and ask it to take the spec apart with this specific prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Take this SDD apart. Give me 20 points that are underspecified, weird, or inconsistent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These questions I copy back into the original Gemini context to improve the SDD. I always return to the original context because it has the full history of how we arrived at the spec—context that would be lost in the critique window. We play this back-and-forth game for 3-5 rounds. Often Master-Gemini already has the answers as it has more context from previous discussions, sometimes I have to add new thoughts or pick the edge case option.&lt;/p&gt;
&lt;p&gt;As you iterate, you’ll notice the questions become increasingly niche—that’s your signal that the spec is becoming bulletproof. After these rounds, you’ll have a comprehensive SDD, typically around 500 lines, that captures every detail of your vision.&lt;/p&gt;
&lt;h2&gt;From SDD to Implementation&lt;/h2&gt;
&lt;p&gt;Once your SDD is complete, implementation becomes almost trivial:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Save the final spec as &lt;code&gt;docs/spec.md&lt;/code&gt; in your repository&lt;/li&gt;
&lt;li&gt;Open Claude Code and simply prompt: “Build spec.md”&lt;/li&gt;
&lt;li&gt;Let it run for 2-4 hours&lt;/li&gt;
&lt;li&gt;Review and iterate on the results&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The beauty of this approach is that Claude Code doesn’t need complex prompting or hand-holding—the spec contains everything it needs. The clear, detailed specification eliminates ambiguity and lets the AI focus on implementation rather than guessing your intent.&lt;/p&gt;
&lt;p&gt;Here’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/peekaboo/blob/main/docs/spec.md&quot;&gt;an example of the spec&lt;/a&gt; I used to build &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.peekaboo.boo/&quot;&gt;Peekaboo - an MCP to give your agent eyes/screenshots&lt;/a&gt;. The best part about Peekaboo? It uses its own agent to prevent cluttering up your context. From spec to working product: just a few hours of letting Claude Code do its thing.&lt;/p&gt;
&lt;p&gt;Wanna see me in action? Here’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.com/posts/2025/the-future-of-vibe-coding&quot;&gt;a video where I do the whole process from idea to final app&lt;/a&gt;. I used Cursor back then, but would use Claude Code now, as it loops much longer without interrupts. Maybe I’ll use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/&quot;&gt;Gemini CLI&lt;/a&gt; in the future. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1937919798740214023&quot;&gt;AI moves so fast!&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Dealing with Context Loss During Refinement&lt;/h2&gt;
&lt;p&gt;One challenge you might encounter: as you refine your spec through multiple iterations, AI tools can hit their output token limits and start dropping earlier context. This “context amnesia” means critical requirements from your initial spec might disappear in later versions—a problem my friend Bruno Virlet pointed out when using Gemini for spec refinement.&lt;/p&gt;
&lt;p&gt;The solution? Instead of asking for one massive spec revision, break it down:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Generate in logical blocks&lt;/strong&gt;: Request specific sections separately
&lt;ul&gt;
&lt;li&gt;“Generate the architecture overview”&lt;/li&gt;
&lt;li&gt;“Generate the API specifications”&lt;/li&gt;
&lt;li&gt;“Generate the data model requirements”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintain a master document&lt;/strong&gt;: Manually concatenate outputs, ensuring nothing gets lost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a requirements checklist&lt;/strong&gt;: Track key features across iterations&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This block-based approach ensures you maintain control over your spec’s evolution while leveraging AI’s analytical capabilities without losing critical details in the process.&lt;/p&gt;
&lt;h2&gt;The Code is the Spec&lt;/h2&gt;
&lt;p&gt;Heck, I restarted an old side project of mine by simply giving Gemini the whole unfinished SwiftUI project (500k tokens!) and told it to generate an SDD from that, but use web tech for it + rebuild in TypeScript. The code &lt;strong&gt;is&lt;/strong&gt; the spec. (blacked out some secrets, something for a future blog post…)&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/understanding-codebases-with-ai-gemini-workflow/sweetistics-sdd.png&quot; alt=&quot;Software Design Document in Google AI Studio&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2&gt;Alternatives: DeepWiki, RepoMix &amp;amp; Co&lt;/h2&gt;
&lt;p&gt;An honorable mention: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://deepwiki.com/&quot;&gt;DeepWiki&lt;/a&gt; is surprisingly great at understanding a codebase and includes a free agent. For example, here’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://deepwiki.com/amantus-ai/vibetunnel&quot;&gt;VibeTunnel on DeepWiki&lt;/a&gt;. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal&quot;&gt;VibeTunnel is my current focus&lt;/a&gt;, it turns any browser into your terminal to command your agents (like Claude Code or Gemini CLI) on the go.&lt;/p&gt;
&lt;p&gt;The downside: you can’t mix and match multiple repositories, Gemini rules for that.&lt;/p&gt;
&lt;p&gt;Honorable mentions also to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gitingest.com/&quot;&gt;Gitingest&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://repomix.com/&quot;&gt;Repomix&lt;/a&gt;. They have better design, but aren’t as efficient in choosing exactly the files you want. You get better results if you keep Gemini’s context focused.&lt;/p&gt;
&lt;p&gt;As to alternatives to Google’s AI Studio, I haven’t found anything that comes close. OpenAI has great models, but they struggle with generating the comprehensive 500-line markdown files that make this workflow sing. Gemini’s massive context window and willingness to produce detailed documentation is unmatched for this use case.&lt;/p&gt;
&lt;h2&gt;That’s It!&lt;/h2&gt;
&lt;p&gt;There’s a lot of people on Twitter and Mastodon that ask me about my workflows, so whenever I find a process that works for me, I’ll make a blog post and share it with you. Wanna be the first to hear about it? Follow &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twitter.com/steipete&quot;&gt;@steipete&lt;/a&gt; and sign up for my newsletter.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Now go and build something amazing, or join me &amp;amp; the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal#motivation&quot;&gt;VibeTunnel Team&lt;/a&gt;. We’ll make sure your agents won’t know what hit them!&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>stats.store: Privacy-First Sparkle Analytics</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-18-stats-store-privacy-first-sparkle-analytics/</link>
    <guid isPermaLink="false">urn:aggr:item:823e57bcd946c16e49abd96ae6373231d166a76b</guid>
    <atom:link href="https://steipete.me/posts/2025/stats-store-privacy-first-sparkle-analytics/" rel="via" type="text/html"/>
    <pubDate>Wed, 18 Jun 2025 09:00:00 +0000</pubDate>
    <description>How curiosity about VibeTunnel users led me to build stats.store - a free, open source analytics backend for Sparkle using AI tools, all while cooking dinner.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/stats-store/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;: I built a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://stats.store&quot;&gt;free, open source analytics backend&lt;/a&gt; for Sparkle because all existing solutions are ancient, and I wanted to know how many people use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibetunnel.sh&quot;&gt;VibeTunnel&lt;/a&gt; without being creepy about it.&lt;/p&gt;
&lt;p&gt;I’m philosophically against deep app analytics. Everything I make is open source and free, and integrating an analytics SDK just didn’t feel right. Sparkle has this neat little &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sparkle-project.org/documentation/system-profiling/&quot;&gt;system profiling&lt;/a&gt; feature that’s perfect for people like me - it just tells you the macOS version and how many people opened your app this week. No IP addresses, no invasive tracking, just enough to stay motivated.&lt;/p&gt;
&lt;p&gt;The problem? When I checked for backend implementations, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sparkle-project.org/documentation/system-profiling/&quot;&gt;everything was ancient&lt;/a&gt;. So naturally, I thought: “How hard can it be?”&lt;/p&gt;
&lt;h2&gt;Building stats.store with agents&lt;/h2&gt;
&lt;p&gt;I fired up &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://aistudio.google.com/&quot;&gt;Google’s AI Studio&lt;/a&gt; and wrote a simple spec (you can see it on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/stats-store/tree/main/docs/spec.md&quot;&gt;GitHub&lt;/a&gt;). Then I fed it into &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://v0.dev&quot;&gt;v0&lt;/a&gt; - they use Next.js, Supabase, Tailwind, and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ui.shadcn.com/&quot;&gt;shadcn/ui&lt;/a&gt;, a stack I already know and love.&lt;/p&gt;
&lt;p&gt;After about five or six rounds of typing “continue” into v0, I had something workable. But v0 is great for frontend, less so when things get complex. It started making mistakes it couldn’t fix. So I &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/stats-store&quot;&gt;synced to GitHub&lt;/a&gt; and let &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer/&quot;&gt;Claude Code&lt;/a&gt; take over. Claude added tests, dark mode, improved the design, and fixed the API flow.&lt;/p&gt;
&lt;p&gt;Total time? Six hours. All while working on the next VibeTunnel release. Plus an hour picking the perfect domain (I really like stats.store).&lt;/p&gt;
&lt;h2&gt;What is stats.store?&lt;/h2&gt;
&lt;p&gt;It’s exactly what I needed: a modern, privacy-first analytics backend for Sparkle. You get basic update statistics, macOS version distribution, and a clean dashboard - all while respecting user privacy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What gets collected:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System info:&lt;/strong&gt; macOS version, CPU type (Intel/Apple Silicon), Mac model, core count, RAM, system language&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App usage:&lt;/strong&gt; Version numbers, update timestamps, daily unique users (via salted IP hashes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What doesn’t get collected:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No IP addresses, names, or emails&lt;/li&gt;
&lt;li&gt;No usage patterns or behavior tracking&lt;/li&gt;
&lt;li&gt;No location data beyond language preference&lt;/li&gt;
&lt;li&gt;No device IDs or fingerprinting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything is designed to answer simple questions: How many people use my app? What macOS versions should I support? When do people actually update? The whole thing is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/stats-store&quot;&gt;open source and MIT-licensed&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Free for Open Source&lt;/h2&gt;
&lt;p&gt;Here’s the deal: if you’re building open source Mac apps, it’s completely free. I’ll cover the hosting (Vercel and Supabase make this pretty cheap anyway).&lt;/p&gt;
&lt;p&gt;Want in? Just &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;mailto:peter@steipete.me&quot;&gt;email me&lt;/a&gt;. I’ll add your app to the backend - and by backend, I mean I’ll literally go into Supabase and edit the table. We’re keeping it scrappy. All you have to do is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/stats-store#how-it-works-&quot;&gt;redirect the appcast URLs&lt;/a&gt;, stats.store will fetch the correct one and pass them along.&lt;/p&gt;
&lt;p&gt;If enough people join, we’ll have a nice community dataset showing how fast Mac users update their OS and what hardware they’re running. Could be interesting!&lt;/p&gt;
&lt;h2&gt;Try It&lt;/h2&gt;
&lt;p&gt;Check out &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://stats.store&quot;&gt;stats.store&lt;/a&gt;. If you have a Sparkle-based Mac app and want simple, privacy-respecting analytics, let me know. Together we can build something useful for the Mac developer community.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Questions? Hit me up on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twitter.com/steipete&quot;&gt;Twitter&lt;/a&gt; or &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;mailto:peter@steipete.me&quot;&gt;email&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Showing Settings from macOS Menu Bar Items: A 5-Hour Journey</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-17-showing-settings-from-macos-menu-bar-items-a-5-hour-journey/</link>
    <guid isPermaLink="false">urn:aggr:item:88a65b1236a96ab039b1e9155a3ba0a7e927ad2a</guid>
    <atom:link href="https://steipete.me/posts/2025/showing-settings-from-macos-menu-bar-items/" rel="via" type="text/html"/>
    <pubDate>Tue, 17 Jun 2025 11:00:00 +0000</pubDate>
    <description>Why something as simple as showing a settings dialog from a macOS menu bar app took me 5 hours to figure out, and requires 50 lines of code for what should be a one-liner.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/showing-settings-from-macos-menu-bar-items/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Opening a settings window from a macOS menu bar app should be trivial. It’s not. After spending hours debugging, I’m documenting the gotchas to save you the same frustration.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;SwiftUI provides &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/swiftui/settingslink&quot;&gt;&lt;code&gt;SettingsLink&lt;/code&gt;&lt;/a&gt; for opening settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;MenuBarExtra(&amp;quot;Test&amp;quot;, systemImage: &amp;quot;star.fill&amp;quot;) {
    SettingsLink {
        Text(&amp;quot;Open settings&amp;quot;) 
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, right? Except it doesn’t work reliably in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/swiftui/menubarextra&quot;&gt;&lt;code&gt;MenuBarExtra&lt;/code&gt;&lt;/a&gt;. The documentation doesn’t mention this limitation.&lt;/p&gt;
&lt;p&gt;According to Apple’s documentation, &lt;code&gt;SettingsLink&lt;/code&gt; should “open the app’s settings scene when activated.” However, this assumes your app is already active and has proper window management context - assumptions that don’t hold for menu bar apps.&lt;/p&gt;
&lt;h2&gt;Why It Fails&lt;/h2&gt;
&lt;p&gt;Menu bar apps operate differently from regular macOS apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No dock icon by default&lt;/strong&gt; - They use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsapplication/activationpolicy/accessory&quot;&gt;&lt;code&gt;NSApplication.ActivationPolicy.accessory&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not “active” in the traditional sense&lt;/strong&gt; - They don’t appear in the app switcher&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows may appear behind other apps&lt;/strong&gt; - Without proper activation, windows lack focus&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No SwiftUI graph loaded&lt;/strong&gt; - Settings needs some SwiftUI view initalized and the menu bar uses AppKit under the hood.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The root issue is that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsapplication&quot;&gt;&lt;code&gt;NSApplication&lt;/code&gt;&lt;/a&gt; treats menu bar apps as background utilities, not foreground applications. This affects how windows are ordered and receive events.&lt;/p&gt;
&lt;h2&gt;The Evolution of Workarounds&lt;/h2&gt;
&lt;h3&gt;The Old Way&lt;/h3&gt;
&lt;p&gt;This is how it used to work. Private API, but widely used and simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;if #available(macOS 13, *) {
    NSApp.sendAction(Selector((&amp;quot;showSettingsWindow:&amp;quot;)), to: nil, from: nil)
} else {
    // macOS 12 or earlier
    NSApp.sendAction(Selector((&amp;quot;showPreferencesWindow:&amp;quot;)), to: nil, from: nil)
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This stopped working in Sonoma (14) with the error: “Please use SettingsLink for opening the Settings scene.” Apple deprecated these selectors in favor of SwiftUI’s scene-based approach, but didn’t account for the unique challenges of menu bar apps.&lt;/p&gt;
&lt;h3&gt;The openSettings Environment Action&lt;/h3&gt;
&lt;p&gt;Apple provides an &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/swiftui/opensettingsaction&quot;&gt;&lt;code&gt;openSettings&lt;/code&gt; environment action&lt;/a&gt; for programmatic access (available since macOS 14.0+):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;struct MyView: View {
    @Environment(\.openSettings) private var openSettings

    var body: some View {
        Button(&amp;quot;Open Settings&amp;quot;) {
            openSettings()
        }
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This currently works on macOS 15, but doesn’t work on macOS Tahoe (26). The logic needs an existing SwiftUI render tree, and simply calling the environment variable does nothing if none is found.&lt;/p&gt;
&lt;p&gt;The workaround? As horrible as it sounds, a &lt;em&gt;hidden window&lt;/em&gt;. Of course, that comes with its own issues, unless you massage the window that it’s really off-screen and ideally also doesn’t react to touches.&lt;/p&gt;
&lt;h2&gt;Hide &amp;amp; Seek&lt;/h2&gt;
&lt;p&gt;Now, this works, however the window will open in the &lt;em&gt;background&lt;/em&gt;, and no amount of &lt;code&gt;makeKeyAndOrderFront(nil)&lt;/code&gt; will help. Trust me. I (and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer/&quot;&gt;Claude&lt;/a&gt;) tried plenty variations.&lt;/p&gt;
&lt;p&gt;The real reason? macOS doesn’t allow a window to become selected when there’s no Dock icon. And since it’s common to hide the Dock icon for pure Menu Bar apps, that’s a problem.&lt;/p&gt;
&lt;p&gt;The workaround? Show the Dock icon just before calling &lt;code&gt;openSettings()&lt;/code&gt; and then hiding it again. In a way, this is also convenient for the user as the Icon now represents the “app” - the visible window, and once that closes, we hide the Dock icon again. (via calling &lt;code&gt;NSApp.setActivationPolicy(.accessory)&lt;/code&gt;). Of course the whole thing requires some delays to really work, so let me present you the final, working solution (I apologize in advance):&lt;/p&gt;
&lt;h2&gt;The Working Solution&lt;/h2&gt;
&lt;p&gt;Here’s the minimal implementation for macOS 14 and higher, using Swift 6:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Hidden window to provide context
struct HiddenWindowView: View {
    @Environment(\.openSettings) private var openSettings
    
    var body: some View {
        Color.clear
            .frame(width: 1, height: 1)
            .onReceive(NotificationCenter.default.publisher(for: .openSettingsRequest)) { _ in
                Task { @MainActor in
                    // Show dock icon for window focus
                    NSApp.setActivationPolicy(.regular)
                    try? await Task.sleep(for: .milliseconds(100))
                    
                    // Activate and open
                    NSApp.activate(ignoringOtherApps: true)
                    openSettings()
                    
                    // 2. No window ordering - After openSettings(), you might need additional window management to ensure
                    // it comes to front:
                    try? await Task.sleep(for: .milliseconds(200))
                    if let settingsWindow = findSettingsWindow() {
                        settingsWindow.makeKeyAndOrderFront(nil)
                        settingsWindow.orderFrontRegardless()
                    }
                }
            }
            .onReceive(NotificationCenter.default.publisher(for: .settingsWindowClosed)) { _ in
                // Restore menu bar app state when settings closes
                NSApp.setActivationPolicy(.accessory)
            }
    }
}

// Window identifier for settings
static let settingsWindowIdentifier = &amp;quot;com.apple.SwiftUI.Settings&amp;quot;

/// Finds the settings window using multiple detection methods
static func findSettingsWindow() -&amp;gt; NSWindow? {
    // Try multiple methods to find the window
    return NSApp.windows.first { window in
        // Check by identifier
        if window.identifier?.rawValue == settingsWindowIdentifier {
            return true
        }
        
        // Check by title
        if window.isVisible &amp;amp;&amp;amp; window.styleMask.contains(.titled) &amp;amp;&amp;amp;
           (window.title.localizedCaseInsensitiveContains(&amp;quot;settings&amp;quot;) ||
            window.title.localizedCaseInsensitiveContains(&amp;quot;preferences&amp;quot;)) {
            return true
        }
        
        // Check by content view controller type
        if let contentVC = window.contentViewController,
           String(describing: type(of: contentVC)).contains(&amp;quot;Settings&amp;quot;) {
            return true
        }
        
        return false
    }
}

// App structure
@main
struct MenuBarApp: App {
    var body: some Scene {
        MenuBarExtra(&amp;quot;My App&amp;quot;, systemImage: &amp;quot;star.fill&amp;quot;) {
            Button(&amp;quot;Settings...&amp;quot;) {
                NotificationCenter.default.post(name: .openSettingsRequest, object: nil)
            }
            .keyboardShortcut(&amp;quot;,&amp;quot;, modifiers: .command)
        }
        
        // Required Settings scene
        Settings {
            SettingsView()
                .onDisappear {
                    NotificationCenter.default.post(name: .settingsWindowClosed, object: nil)
                }
        }
        
        // Hidden window for context
        Window(&amp;quot;Hidden&amp;quot;, id: &amp;quot;HiddenWindow&amp;quot;) {
            HiddenWindowView()
        }
        .windowResizability(.contentSize)
        .defaultSize(width: 1, height: 1)
    }
}

extension Notification.Name {
    static let openSettingsRequest = Notification.Name(&amp;quot;openSettingsRequest&amp;quot;)
    static let settingsWindowClosed = Notification.Name(&amp;quot;settingsWindowClosed&amp;quot;)
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/foundation/notificationcenter&quot;&gt;&lt;code&gt;NotificationCenter&lt;/code&gt;&lt;/a&gt; approach decouples the menu action from the window context, allowing the hidden window to handle the actual settings opening.&lt;/p&gt;
&lt;p&gt;For a production-ready implementation with all edge cases (yes, there are some more…) handled, see &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel/blob/2a63599ce0b09d139ddc9954f41f2a5840264f9f/mac/VibeTunnel/Utilities/SettingsOpener.swift#L4&quot;&gt;VibeTunnel’s SettingsOpener.swift&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Scene Order Matters (Unfortunately)&lt;/h2&gt;
&lt;p&gt;While implementing @steipete’s solution described above, I (@matejkob) discovered another gotcha: &lt;strong&gt;the order of scenes in your App’s body actually affects whether this workaround functions correctly&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The working example shown earlier has the scenes in this order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;MenuBarExtra&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Settings&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Window&lt;/code&gt; (hidden)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;However, I found that if you arrange them like this (which seems equally logical):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;var body: some Scene {
    MenuBarExtra(&amp;quot;My App&amp;quot;, systemImage: &amp;quot;star.fill&amp;quot;) {
        ContentView()
    }
    .menuBarExtraStyle(.menu)
    
    Settings {
        SettingsView()
            .onDisappear {
                NotificationCenter.default.post(name: .settingsWindowClosed, object: nil)
            }
    }
    
    // Hidden window for settings context
    Window(&amp;quot;Hidden&amp;quot;, id: &amp;quot;HiddenWindow&amp;quot;) {
        HiddenWindowView()
    }
    .windowResizability(.contentSize)
    .defaultSize(width: 100, height: 100)
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hidden window won’t be “visible” to the SwiftUI environment system (at least not on macOS Sequoia 15.5), and the &lt;code&gt;openSettings()&lt;/code&gt; call will fail silently. However, moving the hidden &lt;code&gt;Window&lt;/code&gt; scene &lt;strong&gt;before&lt;/strong&gt; the &lt;code&gt;Settings&lt;/code&gt; scene makes it work perfectly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;var body: some Scene {
    // Hidden window for settings context - MUST come before Settings
    Window(&amp;quot;Hidden&amp;quot;, id: &amp;quot;HiddenWindow&amp;quot;) {
        HiddenWindowView()
    }
    .windowResizability(.contentSize)
    .defaultSize(width: 100, height: 100)
    
    MenuBarExtra(&amp;quot;My App&amp;quot;, systemImage: &amp;quot;star.fill&amp;quot;) {
        ContentView()
    }
    .menuBarExtraStyle(.menu)
    
    Settings {
        SettingsView()
            .onDisappear {
                NotificationCenter.default.post(name: .settingsWindowClosed, object: nil)
            }
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This suggests that SwiftUI’s scene resolution and environment propagation happens in declaration order, and the &lt;code&gt;@Environment(\.openSettings)&lt;/code&gt; action in the hidden window needs its context established before the &lt;code&gt;Settings&lt;/code&gt; scene is processed.&lt;/p&gt;
&lt;p&gt;Whether this is an Xcode build system quirk, a SwiftUI implementation detail, or intended behavior is unclear from Apple’s documentation. What is clear is that &lt;strong&gt;the hidden window must be declared before the Settings scene&lt;/strong&gt; for this workaround to function reliably.&lt;/p&gt;
&lt;p&gt;This adds yet another layer of undocumented complexity to what should be a simple operation - not only do we need a hidden window and activation policy juggling, but we also need to carefully order our scene declarations to avoid mysterious failures.&lt;/p&gt;
&lt;h2&gt;Understanding the Workaround&lt;/h2&gt;
&lt;p&gt;The hidden window serves multiple purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provides a valid window context for the &lt;code&gt;openSettings&lt;/code&gt; environment action&lt;/li&gt;
&lt;li&gt;Allows us to temporarily switch activation policies without visual disruption&lt;/li&gt;
&lt;li&gt;Gives us a place to handle the complex timing orchestration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The dock icon manipulation (switching between &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsapplication/activationpolicy/accessory&quot;&gt;&lt;code&gt;.accessory&lt;/code&gt;&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsapplication/activationpolicy/regular&quot;&gt;&lt;code&gt;.regular&lt;/code&gt;&lt;/a&gt;) is necessary because macOS only brings windows to the front reliably for apps with dock icons.&lt;/p&gt;
&lt;h2&gt;Fin&lt;/h2&gt;
&lt;p&gt;What should be a one-liner in other frameworks requires careful orchestration in SwiftUI. The combination of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/swiftui/menubarextra&quot;&gt;&lt;code&gt;MenuBarExtra&lt;/code&gt;&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/swiftui/settings&quot;&gt;&lt;code&gt;Settings&lt;/code&gt;&lt;/a&gt; scenes, and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/swiftui/opensettingsaction&quot;&gt;&lt;code&gt;openSettings&lt;/code&gt;&lt;/a&gt; wasn’t designed with the unique constraints of menu bar apps in mind.&lt;/p&gt;
&lt;p&gt;This shouldn’t be so hard. Opening a settings window is one of the most basic operations any app needs to perform. The fact that it requires hidden windows, activation policy juggling, and precise timing delays in 2025 is a testament to how menu bar apps remain second-class citizens in SwiftUI. Until Apple addresses these fundamental issues, we’re stuck with these workarounds.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>VibeTunnel: Turn Any Browser into Your Mac&#39;s Terminal</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-16-vibetunnel-turn-any-browser-into-your-mac-s-terminal/</link>
    <guid isPermaLink="false">urn:aggr:item:e11bca2295e409dcc98d4008ddb387ecac774ca9</guid>
    <atom:link href="https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal/" rel="via" type="text/html"/>
    <pubDate>Mon, 16 Jun 2025 11:00:00 +0000</pubDate>
    <description>We built a browser-based terminal controller in one day using Claude Code, named pipes, and Xterm.js. No SSH needed, just open your browser and start typing. Check and command your agents on the go!</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/vibetunnel/hero.jpg&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: We built a browser-based terminal controller working around 24 hours using Claude Code, named pipes, and Xterm.js. No SSH needed, just open your browser and start typing. Check and command your agents on the go!&lt;/p&gt;
&lt;p&gt;What happens when three developers lock themselves in a room for around 24 hours with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/claude-code&quot;&gt;Claude Code&lt;/a&gt; and too much caffeine? You get &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibetunnel.sh&quot;&gt;VibeTunnel&lt;/a&gt; - a browser-based terminal that actually works. Control &amp;amp; command your agents on the go.&lt;/p&gt;
&lt;p&gt;This is the story of how Mario, Armin, and I built VibeTunnel in one marathon session working around 24 hours.&lt;/p&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;We met up for a hackathon without knowing exactly what we wanted to build, but we knew one thing: we were all completely in love with building stuff with AI. As we talked, we realized we shared the same frustration - we all wanted to check on our AI agents and see how far they’d gotten with their tasks.&lt;/p&gt;
&lt;p&gt;This first version lets you control Claude Code remotely - imagine being at lunch and checking if your agent finished that refactoring task, then immediately giving it the next assignment.&lt;/p&gt;
&lt;p&gt;But here’s the thing - we didn’t want to constrain VibeTunnel to just Claude Code. It can control anything in your terminal. Accessing your development machine’s terminal from anywhere shouldn’t require complex SSH setups. We wanted something that just works. And we’re making it &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/vibetunnel&quot;&gt;open source&lt;/a&gt; so everyone can benefit.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/vibetunnel/team.jpg&quot; alt=&quot;The VibeTunnel team: Peter, Armin and Mario (from left to right)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2&gt;The Birth of VibeTunnel&lt;/h2&gt;
&lt;p&gt;It started with Armin’s prototype that piped stdin/stdout to files and used &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://asciinema.org/&quot;&gt;asciinema&lt;/a&gt; for playback. His initial approach was clever - capture terminal output to a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://jsonlines.org/&quot;&gt;JSONL&lt;/a&gt; file that described terminal dimensions and character events, then replay it using asciinema’s player. But it was one-way communication only - no input, no interactivity, just a recording.&lt;/p&gt;
&lt;p&gt;Armin had actually built a similar library two years ago:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I used a library that I wrote two years ago… that library probably took me three or four days because I had to really figure out how pseudo terminals work. I’m pretty sure you could write this whole thing up in probably under an hour at this point. And I didn’t even bother using my library again… I just copy pasted the whole library in and had Claude do the modifications to it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Within hours, we had transformed it into a full bidirectional terminal emulator. The journey from “wouldn’t it be cool if…” to a working prototype showcases what’s possible when you combine the right tools, the right team, and a healthy dose of determination. Here’s how we built it in one intense session working around 24 hours.&lt;/p&gt;
&lt;h2&gt;The Technical Journey&lt;/h2&gt;
&lt;h3&gt;From Asciinema to Xterm.js: The Marathon Pivot&lt;/h3&gt;
&lt;p&gt;Our first major challenge came when we needed a proper &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://unix.stackexchange.com/questions/145050/what-exactly-is-scrollback-and-scrollback-buffer&quot;&gt;scrollback buffer&lt;/a&gt;. The initial asciinema approach had a fatal flaw - no history. You couldn’t scrollback to see previous output, making it useless for any real work. Imagine running a build command and not being able to scroll up to see the errors!&lt;/p&gt;
&lt;p&gt;Mario spent two hours going down a rabbit hole, investigating whether to write his own ANSI sequence renderer. He got surprisingly far - basic text output worked, colors were rendering, cursor movement was… sort of working. But then came the edge cases: double-width characters, complex cursor positioning, alternate screen buffers, and the hundreds of other ANSI escape sequences that real terminals support. It was becoming clear this was a month-long project, not a two-hour hack.&lt;/p&gt;
&lt;p&gt;Mario explained:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I first investigated whether I could write my own ANSI sequence renderer. That kinda worked. But there were so many edge cases that I eventually searched for something else. Armin had been recommending &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://xtermjs.org&quot;&gt;Xterm.js&lt;/a&gt; all along.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;I went through that a bit and thought maybe I can figure that out, but there was no way to get that working. So I went back to Xterm and spent about two hours figuring out how it works and how to massage it so it works in our context as well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mario exclaimed when we finally integrated Xterm.js:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That was pretty fucking complete. It’s a full terminal emulator that runs in the browser, handling all the ANSI escape sequences, cursor positioning, screen clearing - everything a real terminal needs. The magic is in how it works: feed it the raw output from your shell (including all those escape sequences), and it maintains an internal buffer representing exactly what should be displayed. It outputs this buffer with characters, foreground colors, and background colors that renders directly to the DOM. No canvas needed, just divs and spans with the right styling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The only issue? Unicode rendering for things like box-drawing characters. When you start Claude Code, you get that nice orange border made of Unicode box-drawing characters - it currently falls back to ASCII replacements like ’+’ and ’-’ instead of smooth lines.&lt;/p&gt;
&lt;h3&gt;The Streaming Challenge: Six Terminals and You’re Out&lt;/h3&gt;
&lt;p&gt;We chose Server-Sent Events (SSE) for streaming terminal output because it’s simple, well-supported, and doesn’t require WebSocket complexity. Each terminal connects to an endpoint like &lt;code&gt;/api/stream/session-123&lt;/code&gt; and receives a continuous stream of output events. It worked beautifully… until we tried to open a seventh terminal.&lt;/p&gt;
&lt;p&gt;Turns out browsers have a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.geeksforgeeks.org/computer-networks/what-are-max-parallel-http-connections-in-a-browser/&quot;&gt;hard limit of six concurrent connections&lt;/a&gt; to the same domain. It’s an HTTP/1.1 limitation that exists to prevent connection flooding. Each terminal session needs its own stream, which means you can only have six terminals open at once. We discovered this the hard way when terminal number seven just… didn’t work. No errors, no warnings, just a blank screen waiting for a connection that would never come.&lt;/p&gt;
&lt;p&gt;The solution? Multiplexing. Instead of one connection per terminal, we need a single SSE stream that carries data for all terminals. Each message would be tagged with a session ID, and the frontend would route it to the correct terminal display. It’s more complex but would remove the six-terminal limitation entirely. The architecture is already sketched out: a single &lt;code&gt;/api/stream/all&lt;/code&gt; endpoint that broadcasts all terminal updates, with the frontend filtering based on which terminals are actually visible. It’s on the roadmap, right after we fix the input handling quirks.&lt;/p&gt;
&lt;h3&gt;Claude Code: The Secret Weapon (With Battle Scars)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;We wouldn’t even have attempted this without Claude Code. What would have been a week-long project compressed into hours. The ability to say “integrate Xterm.js for terminal emulation” and get working code in minutes is game-changing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Armin put it in perspective:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;20x is not an understatement in terms of how much faster we are with agents. As an example: This button that I added to the UI, which is install the shell command and sudo the user. It wrote shell script. It wrote an Apple script. And then it wrote another thing around it, and it took it two and a half minutes. And for me to figure out how to bring up the right sudo dialogue, which kind of workaround to use to bring this thing in would have been two hours, three hours.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He compared it to his experience at &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sentry.io&quot;&gt;Sentry&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Every year, we had a hack week culture. So every year, we took four days to five days of three to four people working on one project. And, honestly, three, four people working for five days not nearly as impressive in terms of how much stuff you can produce than working around 24 hours with AI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude excels at bootstrapping. Need to integrate a library you’ve never used? Claude will get you 80% there in minutes. Want to understand how Server-Sent Events work? Claude generates a working example faster than you can read the MDN docs.&lt;/p&gt;
&lt;p&gt;The workflow that emerged was fascinating: Claude would generate the initial implementation, Mario would test it, discover the edge cases, then spend significant time refactoring. But here’s the key insight - even with all the fixes needed, we were still moving 5x faster than coding from scratch. It’s not about getting perfect code; it’s about getting &lt;em&gt;something&lt;/em&gt; that works, then iterating rapidly.&lt;/p&gt;
&lt;p&gt;This is what people often don’t really get. It’s not like you do one prompt and this comes out. You work with it. It is a tool. And if you are a really good engineer, it’s rocket fuel for you. But if you don’t know what you’re doing, you still cannot build this.&lt;/p&gt;
&lt;h2&gt;Three Servers, One Purpose: The Polyglot Experiment&lt;/h2&gt;
&lt;p&gt;In true hackathon fashion, we ended up with three server implementations. But it wasn’t planned - it was born from frustration and iteration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Node.js&lt;/strong&gt; - The reference implementation that Mario built first. It’s the most complete, with all the session management, authentication hooks, and error handling. About 400 lines of Express.js that just works. Perfect for developers who want to hack on it immediately - everyone knows Node.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Swift (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://hummingbird.codes&quot;&gt;Hummingbird&lt;/a&gt;)&lt;/strong&gt; - This came next, as we wanted a Swift implementation. But it became a story of frustration. When I asked Armin what the hardest part was, his answer was immediate:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The hardest thing is Xcode. You’re kinda used to being able to remote control a lot of stuff because a lot of it is text based. And xcodebuild sort of runs on a command line, but… the behavior of xcodebuild on a command line on our machine at least doesn’t match the behavior of Xcode play button. And so it kinda shows you how inappropriate the tool is for agentic workflows.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After spending “almost half an hour to get the bare bones things over from a to b,” he was done. Three hours and much frustration later, we had a working Hummingbird server. It’s actually quite elegant - Swift’s async/await makes the code surprisingly readable. But the development experience was painful.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Rust (now the default)&lt;/strong&gt; - This is where things got interesting. Fed up with Xcode’s limitations, Armin pivoted to Rust. The difference was night and day. The iteration speed was incredible - what took 30 minutes of fighting with Xcode took minutes in Rust. The development experience was so superior that we ended up making the Rust version our recommended default engine. It’s not just about performance (though it does use about 10x less memory) - it’s about developer velocity.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We’re keeping all three implementations for educational purposes. As Mario explained:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We should keep both for educational purposes. Actually, all three for educational purposes because you have an aligned implementation of the same back end in three different environments. It’s the same REST API implemented in three different environments - perfect for learning how different ecosystems handle HTTP, async I/O, and process management. But if you’re just looking to use VibeTunnel? Start with the Rust version.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This unplanned polyglot approach revealed interesting patterns. The Rust version forced us to think carefully about lifetimes and session cleanup. The Swift version’s strong typing caught several API inconsistencies (when we could get it to compile). The Node version’s ecosystem made adding features trivial. Each implementation taught us something that improved the others, even if we didn’t set out to build three versions.&lt;/p&gt;
&lt;h2&gt;The Technology Stack: A Beautiful Frankenstein&lt;/h2&gt;
&lt;p&gt;The final stack that emerged from our coding marathon is a testament to pragmatic engineering - we used whatever worked best for each layer.&lt;/p&gt;
&lt;p&gt;The main component that we distribute is a fully native SwiftUI app (with some sprinkles of AppKit) built with Swift 6. It packages all the bells and whistles, uses &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sparkle-project.org/&quot;&gt;Sparkle&lt;/a&gt; for automatic updates, and ships with all the other components needed to make VibeTunnel work seamlessly. This native app ensures a smooth macOS experience while bundling the entire technology stack:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core Process Management&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rust Binary&lt;/strong&gt; - The heart of the system. Controls process spawning, named pipes, and I/O forwarding. The magic happens through Unix named pipes - we use a regular file for stdout (so we can tail and observe it) and a named pipe for stdin, allowing bidirectional communication between the browser and your shell. Why Rust? Because when you’re dealing with system-level process management, you want something that won’t segfault at 2 AM. The binary is remarkably small - about 2MB compiled - and handles all the tricky bits of PTY allocation, signal forwarding, and process lifecycle management.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Armin explained why this is actually harder than SSH:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What is a lot harder is drawing something in not the same terminal. Because what we’re doing here is we’re basically keeping it running the original terminal plus keeping it running in another terminal. And that is harder because, for instance, one of the terminals resizes, then you need to also update the other one. Or you wanna have a scrollback that is longer than what you would normally show.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Terminal Emulation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Xterm.js&lt;/strong&gt; - Full terminal emulation with ANSI support in the browser. It’s the same library that powers VS Code’s terminal, which gave us confidence it could handle real-world usage. The integration was surprisingly smooth once we understood its API. Pro tip: the documentation is extensive but the examples are gold.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Frontend Framework&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://lit.dev&quot;&gt;Lit Framework&lt;/a&gt;&lt;/strong&gt; - Google’s lightweight web components library. No build step required! This was crucial for our rapid iteration. Just save the file and reload. Lit’s reactive properties made state management trivial, and web components meant our terminal widget was completely self-contained. You could drop it into any webpage and it would just work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Communication Layer&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://en.wikipedia.org/wiki/Server-sent_events&quot;&gt;Server-Sent Events (SSE)&lt;/a&gt;&lt;/strong&gt; - For streaming terminal output. We chose SSE over WebSockets because it’s simpler, works through proxies better, and automatic reconnection is built-in. The unidirectional nature (server to client only) perfectly matched our needs for output streaming.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Backend Options&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js/Rust/Swift&lt;/strong&gt; - Pick your flavor based on your team’s expertise or deployment constraints. They all expose the same REST API, so switching between them is literally just changing a command-line flag.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Real MVP: Teamwork, Claude, and Caffeine&lt;/h2&gt;
&lt;p&gt;This project happened because of a perfect storm of factors:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Armin’s systems wizardry&lt;/strong&gt; - He cranked out the Rust binary in 2-3 hours, building the critical process management layer that makes everything possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mario’s frontend adventures with Claude&lt;/strong&gt; - Mario rebuilt the UI layer three times. The first version was a mess. The second used vanilla JavaScript and quickly became unmaintainable. The third, using Lit, was the charm. Claude was his constant companion, generating boilerplate, explaining APIs, and occasionally leading him astray with over-engineered solutions. The key was learning when to trust Claude and when to take control.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My deep knowledge of macOS and Product-market fit&lt;/strong&gt; - I’ve been in the iOS and macOS space for almost 20 years and really know how to build great products. I could reuse a lot of my existing work to get us really far, especially with branding, distribution, and the difficulties around notarization and updating. I built the app, onboarding, website, the social pages, did the design, and the overall branding to make this from a quick hack project into an actual beautifully designed product.&lt;/p&gt;
&lt;p&gt;I had the most fun building the website with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://v0.dev/&quot;&gt;v0 from Vercel&lt;/a&gt;. I almost didn’t believe that it would shoot out something that is as cool as this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The power of a deadline&lt;/strong&gt; - We worked around 24 hours on this. There’s something magical about a time constraint. It forces pragmatic decisions.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The individual components aren’t really complex. It’s just fitting them together and making them work together. This became our mantra. Named pipes? Simple. SSE? Straightforward. Terminal emulation? Solved problem. But making them dance together in harmony? That’s where the magic (and the bugs) lived.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mario summed up the project perfectly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We can definitely say we wouldn’t even have attempted this without Claude Code… That would be a multi week project probably. Maybe not a multi week project, but definitely a week project.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The real lesson here is about momentum. Once we had that first character appear in the browser - just a simple ‘h’ from typing ‘hello’ - we were hooked. Each small victory fueled the next. Input working? Let’s add colors. Colors working? How about cursor movement. Before we knew it, we had a full terminal emulator.&lt;/p&gt;
&lt;h2&gt;Conclusion: Shipping Beats Perfect&lt;/h2&gt;
&lt;p&gt;VibeTunnel is what happens when developers scratch their own itch with modern tools. We’re making it open source because we want to use this ourselves, and there’s nothing quite like it out there. It’s not perfect - yet already amazing and fun to use.&lt;/p&gt;
&lt;p&gt;As Armin noted about the quality:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I don’t think that we wrote the most amazing code with Claude. There’s definitely a lot of slop in there. But I think if one were to want to make this really, really nice, you could actually use Claude to fix a ton of this stuff. Plus - it’s not just the app that is there. Right? There’s the logo. There’s the website. There is the readme. There’s the documentation. All of it just came out of effectively an agent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The scale of what we built working around 24 hours? Over 16,000 lines of code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Swift: 7,666 lines (57%)&lt;/li&gt;
&lt;li&gt;Rust: 3,001 lines (22%)&lt;/li&gt;
&lt;li&gt;Shell Scripts: 2,331 lines (17%)&lt;/li&gt;
&lt;li&gt;TypeScript: 2,756 lines&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total Core Code: 16,283 lines&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At the end of our marathon session, we all agreed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This was a really fun project. And isn’t that what hacking is all about?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Try &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibetunnel.sh&quot;&gt;VibeTunnel&lt;/a&gt; today! Your agent is waiting in your browser.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-15-vibe-meter-2-0-calculating-claude-code-usage-with-token/</link>
    <guid isPermaLink="false">urn:aggr:item:cd64d5c05abba6ceef1c0abcbec24f9e6f0ff8f4</guid>
    <atom:link href="https://steipete.me/posts/2025/vibe-meter-2-claude-code-usage-calculation/" rel="via" type="text/html"/>
    <pubDate>Sun, 15 Jun 2025 18:00:00 +0000</pubDate>
    <description>How I built support for Anthropic Claude subscriptions in Vibe Meter 2.0, including token counting, SIMD operations, and the challenges of calculating API usage without official APIs.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/vibe-meter-2-claude-code-usage-calculation/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Built Vibe Meter 2.0 to track Claude Code subscription usage by parsing JSON-L files and counting tokens with a SIMD-accelerated BPE tokenizer. No official API exists, so I approximated usage by counting every token. The project grew to 47K lines of Swift with 92% test coverage, all written with CC using “thinking” triggers and modern Swift idioms.&lt;/p&gt;
&lt;h2&gt;The Shock: How Much Am I Actually Using?&lt;/h2&gt;
&lt;p&gt;What started as a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/the-future-of-vibe-coding&quot;&gt;simple idea&lt;/a&gt; to explain vibe coding to new people is slowly growing up. With version 2 of Vibe Meter, I added full support for Anthropic’s subscription. While v1 only focused on cost display for Cursor, for version 2 I wanted to add support for CC. This turned out to be much more of an adventure than I initially thought.&lt;/p&gt;
&lt;h2&gt;The Problem: Flying Blind Without APIs&lt;/h2&gt;
&lt;p&gt;Anthropic has both API endpoints with straightforward pricing. They also offer subscriptions where especially in the max version you get around 900 requests in a 5-hour window. I was curious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A)&lt;/strong&gt; How much that would actually cost if it would be API calls&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B)&lt;/strong&gt; How many requests you actually have open in that 5-hour window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/vibe-meter-2-claude-code-usage-calculation/claude-code-usage-report.png&quot; alt=&quot;Claude Code Usage Report showing token counts and cost calculations&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;Well, turns out this is not easy at all. There is no API for that, there’s no cost control, there’s not even a hidden private API or something on the website that you could look at. The only way literally is to count the individual tokens that are sent to the LLM and then approximate how much is left. We also have to approximate when a window starts. As you can see, this was the start of a little adventure.&lt;/p&gt;
&lt;h2&gt;The Solution Journey: From Collaboration to SIMD&lt;/h2&gt;
&lt;p&gt;Luckily, I wasn’t alone. There were other people that were already curious and interested in that idea, so I could learn from various open source projects like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/ryoppippi/ccusage&quot;&gt;ccusage&lt;/a&gt;, which pioneered the approach of parsing Claude’s JSON-L files. Counting tokens also is not as simple as you would think. Claude Code writes all interactions with their server into so-called JSON-L files which can be in the hundreds of megabytes large. So I spent a lot of time tweaking the parser, or dare I say me and Claude did.&lt;/p&gt;
&lt;h3&gt;SIMD Operations for Performance&lt;/h3&gt;
&lt;p&gt;During the process I even started using SIMD operations, something which I only darkly remember got support in Swift but now it’s crucial to offer a speedy parsing that doesn’t bog down your CPU.&lt;/p&gt;
&lt;p&gt;The most impressive optimization is the vectorized lookup table that uses SIMD16 (16-byte vectors) for fast pattern matching:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// VibeMeter/Core/Utilities/Tiktoken/CoreBPESIMD.swift:222-281
private struct VectorizedBytePairLookup {
    // Pre-computed SIMD vectors for common byte pairs
    private let pairVectors: [SIMD16&amp;lt;UInt8&amp;gt;: [(length: Int, rank: Int)]]

    init(bytePairRanks: [Data: Int]) {
        // Build SIMD lookup table for byte sequences
        for (data, rank) in bytePairRanks {
            if data.count &amp;gt; 16 { continue } // Skip sequences longer than SIMD width

            // Create a SIMD vector padded with zeros
            var vector = SIMD16&amp;lt;UInt8&amp;gt;(repeating: 0)
            data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
                for i in 0 ..&amp;lt; min(data.count, 16) {
                    vector[i] = bytes[i]
                }
            }
            vectors[vector]?.append((length: data.count, rank: rank))
        }
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach allows us to compare 16 bytes at once instead of doing byte-by-byte comparisons, dramatically speeding up the token counting process.&lt;/p&gt;
&lt;h3&gt;Token Counting with BPE&lt;/h3&gt;
&lt;p&gt;As a basis, I used the open source &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/openai/tiktoken&quot;&gt;TikToken BPE tokenizer&lt;/a&gt; that is actually from OpenAI. There’s no public tokenizer project from Anthropic. However, the tokens are similar enough that we can use this project and still get fairly accurate results.&lt;/p&gt;
&lt;p&gt;BPE (Byte Pair Encoding) is a compression algorithm adapted for tokenization in NLP. It breaks text into subword units (tokens), often capturing whole words or common prefixes/suffixes. Instead of just splitting on whitespace or characters, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://en.wikipedia.org/wiki/Byte-pair_encoding&quot;&gt;BPE&lt;/a&gt; builds a vocabulary of frequently seen pairs of characters or subwords. This allows models to handle unknown words, rare terms, and typos more gracefully.&lt;/p&gt;
&lt;h2&gt;Key Insights: Thinking Triggers and Fresh Context&lt;/h2&gt;
&lt;p&gt;Agentic engineering, as I prefer to say, requires even more clear thinking. I evolved my approach where I would use Claude Code and ask it to think hard about the problem. The thinking keyword is key because it triggers how much token Claude uses to actually think about the problem. There are various levels how you can trigger thinking, with Ultra Think being the highest level.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As I work more with Claude Code, I tend to slow down, ask it to plan, present options, ultrathink, and then pick the best one. And always using a fresh context. Much better results.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Look at these examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/commit/4e447e8e19a65136c01b31e264440f119af40b9a&quot;&gt;Debounce property wrapper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/commit/b71a484f2d1484c77c6466d38612b92a64c546af&quot;&gt;SIMD-accelerated BPE encoder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Took a few planning prompts each but then the code one-shots and works perfect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip&lt;/strong&gt;: One trick to get better results in Claude Code is to use the various “thinking” triggers, with ultrathink being the boss. Check out the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/extended-thinking-tips&quot;&gt;extended thinking tips&lt;/a&gt; in the Anthropic documentation for more details.&lt;/p&gt;
&lt;h3&gt;🚀 Side Quest: The Birth of llm.codes&lt;/h3&gt;
&lt;p&gt;While building Vibe Meter 2.0, I ran into a weird issue where Claude would argue with me that one cannot build nice toolbars in SwiftUI. This ultimately inspired me to build llm.codes, a way to feed Claude up-to-date information so it produces better code. You can read more about this journey in my post: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/llm-codes-transform-developer-docs&quot;&gt;llm.codes: Transform Your Developer Docs into LLM-Optimized References&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Modern Swift Refactoring&lt;/h2&gt;
&lt;p&gt;In the 2.0 refactor, I also put a better effort on using modern Swift idioms, and most I did that by Claude teaching modern Swift via a new Markdown file that I fed to him and asked him to refactor my codebase using modern Swift idioms. I shared this and many other knowledge files in my new &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/agent-rules/tree/main/docs&quot;&gt;agent rules repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Results: 47K Lines, 92% Coverage&lt;/h2&gt;
&lt;p&gt;VibeMeter has grown into a substantial project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;App code&lt;/strong&gt;: 24,529 lines&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test code&lt;/strong&gt;: 22,560 lines&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total&lt;/strong&gt;: 47,089 lines across 218 Swift files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test coverage ratio&lt;/strong&gt;: 92% (tests are nearly as large as the app itself!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this release, I also added support for macOS 14&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fnref-1&quot; data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt; which was quite simple.&lt;/p&gt;
&lt;h2&gt;Get Vibe Meter 2.0&lt;/h2&gt;
&lt;p&gt;Note: Vibe Meter is still on the pre-release channel for now, there’s a few edge-cases I wanna get right before making it 2.0 official, but it’s close!&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibemeter.ai/&quot;&gt;Get it here&lt;/a&gt; and switch update channel for the preview!&lt;/p&gt;
&lt;section class=&quot;footnotes&quot; data-footnotes&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;The only issue was with automatic observation tracking, which is macOS 15+ only. Learn more about this hidden gem in my post: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/automatic-observation-tracking-uikit-appkit&quot;&gt;Automatic Observation Tracking in UIKit and AppKit&lt;/a&gt;. &lt;a href=&quot;#fnref-1&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;1&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>llm.codes: Make Apple Docs AI-Readable</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-14-llm-codes-make-apple-docs-ai-readable/</link>
    <guid isPermaLink="false">urn:aggr:item:405e124766d1b88cd6685e5b79e32141944db96d</guid>
    <atom:link href="https://steipete.me/posts/2025/llm-codes-transform-developer-docs/" rel="via" type="text/html"/>
    <pubDate>Sat, 14 Jun 2025 13:22:16 +0000</pubDate>
    <description>Built this when Claude couldn&#39;t read Apple&#39;s docs. Now it converts 69+ documentation sites to clean llms.txt. Free, instant, no BS.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/llm-codes-transform-developer-docs/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://llm.codes&quot;&gt;llm.codes&lt;/a&gt; converts JavaScript-heavy Apple docs (and 69+ other sites) into a clean llms.txt that AI agents can actually read.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Start&lt;/strong&gt;: Try it now with Apple’s Foundation Models docs: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://llm.codes?https://developer.apple.com/documentation/foundationmodels&quot;&gt;llm.codes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even the smartest models can’t fetch fresh docs - especially when the docs are hidden behind JavaScript. While working on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibemeter.ai/&quot;&gt;Vibe Meter&lt;/a&gt;, Claude tried to convince me that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1933819029224931619&quot;&gt;it wasn’t possible to make a proper toolbar in SwiftUI&lt;/a&gt; and went down to AppKit. Even when I asked it to google for a solution, nothing changed.&lt;/p&gt;
&lt;h2&gt;The Real Problem: JavaScript-Heavy Documentation&lt;/h2&gt;
&lt;p&gt;The core issue? &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/swiftui/&quot;&gt;Apple’s documentation heavily uses JavaScript&lt;/a&gt;, and Claude Code (or most AI agents to date) simply cannot parse that. It will fail and see nothing. So if you’re working with a component where documentation only exists on JavaScript-rendered pages, you’re completely stuck.&lt;/p&gt;
&lt;h2&gt;Enter llm.codes&lt;/h2&gt;
&lt;p&gt;That’s when I built the docs converter. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://llm.codes&quot;&gt;llm.codes&lt;/a&gt; allows you to point to documentation and fetch everything as clean Markdown. While it’s optimized for Apple documentation, it supports a wide range of developer documentation sites. Here’s what you get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your AI can finally see Apple docs&lt;/strong&gt; - No blind spots from JavaScript pages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;70% smaller files&lt;/strong&gt; - More context space for your actual code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Works with 69+ sites&lt;/strong&gt; - AWS, Tailwind, PyTorch, PostgreSQL, and more&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Supported Documentation Sites&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mobile Development&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apple Developer Documentation&lt;/li&gt;
&lt;li&gt;Android Developer Documentation&lt;/li&gt;
&lt;li&gt;React Native&lt;/li&gt;
&lt;li&gt;Flutter&lt;/li&gt;
&lt;li&gt;Swift Package Index&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Programming Languages&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python, TypeScript, JavaScript (MDN), Rust, Go, Java, Ruby, PHP, Swift, Kotlin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Web Frameworks&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React, Vue.js, Angular, Next.js, Nuxt, Svelte, Django, Flask, Express.js, Laravel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cloud Platforms&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS, Google Cloud, Azure, DigitalOcean, Heroku, Vercel, Netlify&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Databases&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL, MongoDB, MySQL, Redis, Elasticsearch, Couchbase, Cassandra&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DevOps &amp;amp; Infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker, Kubernetes, Terraform, Ansible, GitHub, GitLab&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;AI/ML Libraries&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PyTorch, TensorFlow, Hugging Face, scikit-learn, LangChain, pandas, NumPy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;CSS Frameworks&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tailwind CSS, Bootstrap, Material-UI, Chakra UI, Bulma&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Build Tools &amp;amp; Testing&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;npm, webpack, Vite, pip, Cargo, Maven, Jest, Cypress, Playwright, pytest&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;And more&lt;/strong&gt;: Any GitHub Pages site (*.github.io)&lt;/p&gt;
&lt;p&gt;llm.codes uses &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.firecrawl.dev/referral?rid=9CG538BE&quot;&gt;Firecrawl&lt;/a&gt; under the hood, and I pay for the credits to keep this service free for everyone.&lt;/p&gt;
&lt;h2&gt;Real-World Example&lt;/h2&gt;
&lt;p&gt;Remember my toolbar problem? &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1933819029224931619&quot;&gt;Here’s what happened&lt;/a&gt;: I dragged the generated SwiftUI markdown from &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/agent-rules/blob/main/docs/swiftui.md&quot;&gt;my agent-rules repository&lt;/a&gt; into the terminal, and suddenly Claude wrote exactly the code I wanted.&lt;/p&gt;
&lt;p&gt;The key insight: When you work on a component, just ask Claude to read the docs. It will load everything it needs into its context and produce vastly better code.&lt;/p&gt;
&lt;p&gt;For people who think &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/Context7AI&quot;&gt;@Context7&lt;/a&gt; is the answer: if you use the context7 mcp for SwiftUI, you get &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://context7.com/ivanvorobei/swiftui&quot;&gt;sample code from 2019&lt;/a&gt;, which will produce horribly outdated code. You need current documentation, not ancient examples.&lt;/p&gt;
&lt;h2&gt;Beyond Just llm.codes&lt;/h2&gt;
&lt;p&gt;I used this trick before in my post about &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/migrating-700-tests-to-swift-testing&quot;&gt;migrating 700 tests to Swift Testing&lt;/a&gt;. With llm.codes, you get significantly smaller markdown files, which preserves more token context space for your agent.&lt;/p&gt;
&lt;p&gt;I also maintain a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/agent-rules/tree/main/docs&quot;&gt;collection of pre-converted Markdown documentation files&lt;/a&gt; in my agent-rules repository, that go beyond just documentation.&lt;/p&gt;
&lt;p&gt;The converter itself? Completely vibe-coded with Claude and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/amantus-ai/llm-codes&quot;&gt;open source&lt;/a&gt;. I chose the stack (Next.js, Tailwind, Vercel) but didn’t write a single line of TypeScript-and it worked beautifully on the first try.&lt;/p&gt;
&lt;h2&gt;Try It Out&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://llm.codes?https://developer.apple.com/documentation/foundationmodels&quot;&gt;Convert a page →&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No sign-up needed&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;AI agents are the future of coding. Until docs catch up, llm.codes is your bridge to that future.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-11-automatic-observation-tracking-in-uikit-and-appkit-the/</link>
    <guid isPermaLink="false">urn:aggr:item:79062a9d4f3c8f319151b971c9c50a35645ab600</guid>
    <atom:link href="https://steipete.me/posts/2025/automatic-observation-tracking-uikit-appkit/" rel="via" type="text/html"/>
    <pubDate>Wed, 11 Jun 2025 09:00:00 +0000</pubDate>
    <description>Discover how iOS 18&#39;s hidden automatic observation tracking brings SwiftUI-like reactive programming to UIKit and AppKit, making your UI code cleaner and more maintainable.</description>
    <content:encoded>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: iOS 18 and macOS 15 secretly ship with automatic observation tracking for UIKit/AppKit. Enable it with a plist key, and your views magically update when your &lt;code&gt;@Observable&lt;/code&gt; models change. No more manual &lt;code&gt;setNeedsDisplay()&lt;/code&gt; calls!&lt;/p&gt;
&lt;p&gt;Remember when SwiftUI came out and we all marveled at how views automatically updated when &lt;code&gt;@Published&lt;/code&gt; properties changed? Well, Apple has been quietly working on bringing that same magic to UIKit and AppKit. The best part? It shipped in iOS 18/macOS 15, but hardly anyone knows about it. You don’t even need Xcode 26, it’s just one simple plist entry away.&lt;/p&gt;
&lt;h2&gt;The Problem We’ve All Faced&lt;/h2&gt;
&lt;p&gt;Let’s be honest - keeping your UI in sync with your data model in UIKit has always been a chore. Here’s the dance we’ve all done:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;class ProfileViewController: UIViewController {
    var user: User? {
        didSet {
            updateUI()
        }
    }
    
    func updateUI() {
        nameLabel.text = user?.name
        avatarImageView.image = user?.avatar
        // ... 20 more lines of manual updates
        setNeedsLayout()
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Forgot to call &lt;code&gt;updateUI()&lt;/code&gt;? Enjoy your stale UI. Called it too often? Hello, performance issues. It’s tedious and error-prone.&lt;/p&gt;
&lt;h2&gt;Enter Automatic Observation Tracking&lt;/h2&gt;
&lt;p&gt;With the new observation framework, this entire pattern becomes obsolete. Here’s the same code with automatic tracking:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;import Observation

@Observable
class User {
    var name: String = &amp;quot;&amp;quot;
    var avatar: UIImage?
    var unreadCount = 0
    
    var hasUnread: Bool {
        unreadCount &amp;gt; 0
    }
}

class ProfileViewController: UIViewController {
    let user = User()
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        // UIKit tracks these property accesses automatically!
        nameLabel.text = user.name
        avatarImageView.image = user.avatar
        badgeView.isHidden = !user.hasUnread
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. Change &lt;code&gt;user.name&lt;/code&gt; anywhere in your app, and the label updates. No manual calls, no forgotten updates, no performance overhead from unnecessary refreshes. It just works.&lt;/p&gt;
&lt;h2&gt;Where Observation Tracking Works&lt;/h2&gt;
&lt;p&gt;The automatic observation tracking is supported in a variety of UIKit and AppKit methods. For most cases, &lt;code&gt;viewWillLayoutSubviews()&lt;/code&gt; in UIKit view controllers, &lt;code&gt;layoutSubviews()&lt;/code&gt; in UIKit views, and their AppKit equivalents (&lt;code&gt;viewWillLayout()&lt;/code&gt; and &lt;code&gt;layout()&lt;/code&gt;) are the go-to choices.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;View the complete list of supported methods&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;UIView&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/automatic-trait-tracking&quot;&gt;&lt;code&gt;updateProperties()&lt;/code&gt;&lt;/a&gt; (iOS 26+)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews&quot;&gt;&lt;code&gt;layoutSubviews()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints&quot;&gt;&lt;code&gt;updateConstraints()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiview/1622529-draw&quot;&gt;&lt;code&gt;draw(_:)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UIViewController&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/automatic-trait-tracking&quot;&gt;&lt;code&gt;updateProperties()&lt;/code&gt;&lt;/a&gt; (iOS 26+)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiviewcontroller/1621437-viewwilllayoutsubviews&quot;&gt;&lt;code&gt;viewWillLayoutSubviews()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiviewcontroller/1621398-viewdidlayoutsubviews&quot;&gt;&lt;code&gt;viewDidLayoutSubviews()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiviewcontroller/1621379-updateviewconstraints&quot;&gt;&lt;code&gt;updateViewConstraints()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiviewcontroller/4195484-updatecontentunavailableconfigur&quot;&gt;&lt;code&gt;updateContentUnavailableConfiguration(using:)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UIPresentationController&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618335-containerviewwilllayoutsubviews&quot;&gt;&lt;code&gt;containerViewWillLayoutSubviews()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618329-containerviewdidlayoutsubviews&quot;&gt;&lt;code&gt;containerViewDidLayoutSubviews()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UIButton&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uibutton/3750773-updateconfiguration&quot;&gt;&lt;code&gt;updateConfiguration()&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When executing the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uibutton/3750772-configurationupdatehandler&quot;&gt;&lt;code&gt;configurationUpdateHandler&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600950-updateconfiguration&quot;&gt;&lt;code&gt;updateConfiguration(using:)&lt;/code&gt;&lt;/a&gt; (UICollectionViewCell)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uitableviewcell/3601058-updateconfiguration&quot;&gt;&lt;code&gt;updateConfiguration(using:)&lt;/code&gt;&lt;/a&gt; (UITableViewCell)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uitableviewheaderfooterview/3601061-updateconfiguration&quot;&gt;&lt;code&gt;updateConfiguration(using:)&lt;/code&gt;&lt;/a&gt; (UITableViewHeaderFooterView)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When executing the &lt;code&gt;configurationUpdateHandler&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;NSView (AppKit)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsview/1526891-updateconstraints&quot;&gt;&lt;code&gt;updateConstraints()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsview/1526146-layout&quot;&gt;&lt;code&gt;layout()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsview/1483686-draw&quot;&gt;&lt;code&gt;draw(_:)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsview/1526068-updatelayer&quot;&gt;&lt;code&gt;updateLayer()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;NSViewController (AppKit)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsviewcontroller/1434401-updateviewconstraints&quot;&gt;&lt;code&gt;updateViewConstraints()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsviewcontroller/1434481-viewwilllayout&quot;&gt;&lt;code&gt;viewWillLayout()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/appkit/nsviewcontroller/1434491-viewdidlayout&quot;&gt;&lt;code&gt;viewDidLayout()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Enabling the Magic&lt;/h2&gt;
&lt;p&gt;Here’s where it gets interesting. This feature isn’t enabled by default (yet). You need to add a key to your Info.plist:&lt;/p&gt;
&lt;h3&gt;For UIKit (iOS 18+)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;UIObservationTrackingEnabled&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;For AppKit (macOS 15+)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;NSObservationTrackingEnabled&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This plist key enables observation tracking in iOS 18 and macOS 15. Starting with their 26 releases, this is on by default and the key will simply be ignored.&lt;/p&gt;
&lt;h2&gt;iOS 26 and Beyond&lt;/h2&gt;
&lt;p&gt;iOS 26 (already in beta!) brings improvements. The new &lt;code&gt;updateProperties()&lt;/code&gt; method on both &lt;code&gt;UIView&lt;/code&gt; and &lt;code&gt;UIViewController&lt;/code&gt; provides an even better place for observable property access. For a comprehensive overview of all iOS 26 UIKit additions, check out &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.swiftjectivec.com/ios-26-notable-uikit-additions/&quot;&gt;Jordan Morgan’s excellent writeup&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;class MyView: UIView {
    let model: MyModel
    
    override func updateProperties() {
        super.updateProperties()
        // This runs before layoutSubviews for even better performance
        backgroundColor = model.backgroundColor
        layer.cornerRadius = model.cornerRadius
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method is specifically designed for property updates and runs before &lt;code&gt;layoutSubviews&lt;/code&gt;, allowing for more efficient updates and clearer separation of concerns.&lt;/p&gt;
&lt;p&gt;Just like the layout system has &lt;code&gt;setNeedsLayout()&lt;/code&gt; and &lt;code&gt;layoutIfNeeded()&lt;/code&gt;, the property update system provides &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiview/setNeedsUpdateProperties&quot;&gt;&lt;code&gt;setNeedsUpdateProperties()&lt;/code&gt;&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/uiview/updatePropertiesIfNeeded&quot;&gt;&lt;code&gt;updatePropertiesIfNeeded()&lt;/code&gt;&lt;/a&gt;. You can call &lt;code&gt;setNeedsUpdateProperties()&lt;/code&gt; to schedule a property update on the next update cycle, or use &lt;code&gt;updatePropertiesIfNeeded()&lt;/code&gt; to force an immediate update if one is pending. This gives you fine-grained control over when property updates occur, which is especially useful for optimizing performance in complex view hierarchies.&lt;/p&gt;
&lt;p&gt;Apple’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/uikit/automatic-trait-tracking&quot;&gt;automatic trait tracking documentation&lt;/a&gt; provides detailed guidance on using these new APIs. Plus, automatic observation tracking is enabled by default in iOS 26, so you won’t even need the plist key anymore.&lt;/p&gt;
&lt;h2&gt;The Gotchas&lt;/h2&gt;
&lt;p&gt;Of course, it’s not all roses. Here are a few things to watch out for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Observation happens in specific methods&lt;/strong&gt;: Only properties accessed in the supported methods (see list above) are tracked&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timing matters&lt;/strong&gt;: If you’re doing expensive computations, consider caching results since these methods can be called frequently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory considerations&lt;/strong&gt;: Observable objects are retained while being observed, so be mindful of retain cycles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread safety&lt;/strong&gt;: While &lt;code&gt;@Observable&lt;/code&gt; is thread-safe, mutations from different threads could lead to inconsistent UI representations. Keep all mutations on the main thread to avoid surprises&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A Pattern to Avoid&lt;/h2&gt;
&lt;p&gt;You might be tempted to create a method that pre-accesses all observable properties:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// ❌ Don&amp;#39;t do this
override func trackObservableProperties() {
    // Accessing all properties upfront
    _ = model.backgroundColor
    _ = model.cornerRadius
    _ = model.title
    // ... etc
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an anti-pattern for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inefficiency&lt;/strong&gt;: It establishes observation dependencies for ALL properties, even those not used in the current UI state. The beauty of automatic observation is that it only tracks properties actually accessed during updates.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fragility&lt;/strong&gt;: You’re maintaining a duplicate list of properties that can easily fall out of sync with your actual UI code.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Instead, access properties directly where they’re used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// ✅ Do this
override func layoutSubviews() {
    super.layoutSubviews()
    // Only accessed properties create dependencies
    if model.isOptionEnabled {
        view.foo = model.bar  // Only bar is observed
    } else {
        view.foo = model.baz  // Only baz is observed
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way, only the properties actually affecting your UI get observation dependencies, making your code both more efficient and maintainable.&lt;/p&gt;
&lt;h2&gt;Performance Considerations&lt;/h2&gt;
&lt;p&gt;You might be wondering about performance. The beauty of this system is that it only tracks dependencies when views are actually laying out. If a view isn’t visible, it’s not tracking. The observation framework uses a sophisticated dependency graph that ensures minimal overhead.&lt;/p&gt;
&lt;h2&gt;Complete Example Project&lt;/h2&gt;
&lt;p&gt;Automatic observation tracking is one of those features that makes you wonder how you lived without it. It brings the best parts of SwiftUI’s reactive programming model to UIKit and AppKit, without requiring a complete rewrite of your app.&lt;/p&gt;
&lt;p&gt;All the code snippets in this post come from a fully working example project. Check it out on GitHub: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/ObservationTrackingExample&quot;&gt;ObservationTrackingExample&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The Missing Piece: Custom Traits&lt;/h2&gt;
&lt;p&gt;If you’ve used SwiftUI, you know the joy of &lt;code&gt;@EnvironmentObject&lt;/code&gt; - drop an object at the root, access it anywhere. UIKit developers have been jealous of this pattern for years. Well, jealous no more. (Mac devs miss out tho - there’s no equivalent on AppKit yet)&lt;/p&gt;
&lt;p&gt;Since iOS 17, UIKit has quietly introduced custom traits - a way to attach arbitrary values to the trait collection that flows through your view hierarchy. These aren’t just for dark mode and size classes anymore. Keith Harrison has an &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://useyourloaf.com/blog/custom-traits-and-swiftui/&quot;&gt;excellent deep dive into custom traits&lt;/a&gt; if you want the full story.&lt;/p&gt;
&lt;p&gt;The magic happens when you combine custom traits with observable objects. You get automatic propagation AND automatic updates. It’s like having your cake and eating it too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;View the complete Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s build an app-wide state container that any view can access and observe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;import UIKit
import Observation

@Observable 
class AppModel {
    var currentUser: User?
    var theme: Theme = .light
    var isOnline = true
    
    // Add more app-wide state as needed
}

// Define a custom trait for your app model
struct AppModelTrait: UITraitDefinition {
    static let defaultValue: AppModel? = nil
}

// Add convenient accessors
extension UITraitCollection {
    var appModel: AppModel? {
        self[AppModelTrait.self]
    }
}

extension UIMutableTraits {
    var appModel: AppModel? {
        get { self[AppModelTrait.self] }
        set { self[AppModelTrait.self] = newValue }
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Injecting the Model&lt;/h2&gt;
&lt;p&gt;At your app’s root (usually in your scene delegate or root view controller), inject the model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    let appModel = AppModel()
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        let rootViewController = MainTabBarController()
        
        // Inject the app model into the trait system
        rootViewController.traitOverrides.appModel = appModel
        
        window?.rootViewController = rootViewController
        window?.makeKeyAndVisible()
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Observing Changes Anywhere&lt;/h2&gt;
&lt;p&gt;Now the magic part - any view controller in your hierarchy can access and observe the model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;class ProfileViewController: UIViewController {
    let nameLabel = UILabel()
    let statusIndicator = UIView()
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        guard let model = traitCollection.appModel else { return }
        
        // These automatically update when model properties change!
        nameLabel.text = model.currentUser?.name ?? &amp;quot;Guest&amp;quot;
        statusIndicator.backgroundColor = model.isOnline ? .systemGreen : .systemRed
        
        // Theme updates
        view.backgroundColor = model.theme.backgroundColor
        nameLabel.textColor = model.theme.textColor
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No delegates. No notifications. No manual updates. Change &lt;code&gt;appModel.currentUser&lt;/code&gt; anywhere in your app, and every view observing it updates automatically.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/observation&quot;&gt;Apple’s Observation Framework Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/videos/play/wwdc2023/10149/&quot;&gt;WWDC 2023: Discover Observation in Swift&lt;/a&gt; (covers the foundation)&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/ObservationTrackingExample&quot;&gt;Example Project on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://forums.swift.org/t/observation-tracking-in-uikit&quot;&gt;Swift Forums Discussion on Observation Tracking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://useyourloaf.com/blog/custom-traits-and-swiftui/&quot;&gt;Custom Traits and SwiftUI&lt;/a&gt; by Keith Harrison&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Peekaboo MCP – lightning-fast macOS screenshots for AI agents</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-07-peekaboo-mcp-lightning-fast-macos-screenshots-for-ai-agents/</link>
    <guid isPermaLink="false">urn:aggr:item:90ff2867051bc650518fbaabb38c5acbe5df5126</guid>
    <atom:link href="https://steipete.me/posts/2025/peekaboo-mcp-lightning-fast-macos-screenshots-for-ai-agents/" rel="via" type="text/html"/>
    <pubDate>Sat, 7 Jun 2025 11:00:00 +0000</pubDate>
    <description>Turn your blind AI into a visual debugger with instant screenshot capture and analysis</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/peekaboo-mcp-lightning-fast-macos-screenshots-for-ai-agents/banner.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Peekaboo is a macOS-only MCP server that enables AI agents to capture screenshots of applications, or the entire system, with optional visual question answering through local or remote AI models.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Without screenshots, agents debug blind—Peekaboo gives them eyes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What Peekaboo Can Do&lt;/h2&gt;
&lt;p&gt;Peekaboo provides three main tools that give AI agents visual capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;image&lt;/code&gt;&lt;/strong&gt; - Capture screenshots of screens or specific applications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;analyze&lt;/code&gt;&lt;/strong&gt; - Ask AI questions about captured images using vision models&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;list&lt;/code&gt;&lt;/strong&gt; - Enumerate available screens and windows for targeted captures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each tool is designed to be powerful and flexible. The most powerful feature is visual question answering - agents can ask questions about screenshots like “What do you see in this window?” or “Is the submit button visible?” and get accurate answers. This saves context space since asking specific questions is much more efficient than returning raw image data.&lt;/p&gt;
&lt;p&gt;Peekaboo supports both cloud and local vision models, letting you choose between accuracy and privacy.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://cursor.com/deeplink/mcp-install-dark.png&quot; alt=&quot;Install Peekaboo in Cursor IDE&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt; &lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://cursor.com/deeplink/mcp-install-light.png&quot; alt=&quot;Install Peekaboo in Cursor IDE&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h2&gt;Design Philosophy&lt;/h2&gt;
&lt;h3&gt;Less is More&lt;/h3&gt;
&lt;p&gt;The most important rule when building MCPs: &lt;strong&gt;Keep the number of tools small&lt;/strong&gt;. Most agents struggle once they encounter more than 40 different tools. My approach is to make every tool very powerful but keep the total count minimal to avoid cluttering the context.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/peekaboo-mcp-lightning-fast-macos-screenshots-for-ai-agents/cursor-40-tools.png&quot; alt=&quot;Cursor showing 40+ tools can become overwhelming&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h3&gt;Lenient Tool Calling&lt;/h3&gt;
&lt;p&gt;Another crucial principle: &lt;strong&gt;tool calling should be lenient&lt;/strong&gt;. Agents make mistakes with parameters, so rather than returning errors, Peekaboo tries to understand their intent. Being overly strict just forces unnecessary retry loops - MCPs should be forgiving since agents aren’t infallible.&lt;/p&gt;
&lt;h3&gt;Fuzzy Window Matching&lt;/h3&gt;
&lt;p&gt;Peekaboo implements fuzzy window matching because agents don’t always know exact window titles. If an agent asks for “Chrome” but the window is titled “Google Chrome - Peekaboo MCP”, we still match it. Partial matches work, case doesn’t matter, and common variations are understood.&lt;/p&gt;
&lt;p&gt;For more insights on building robust MCP tools, check out my guide: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/mcp-best-practices&quot;&gt;MCP Best Practices&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Local vs Cloud Vision Models&lt;/h2&gt;
&lt;p&gt;Peekaboo supports both local and cloud vision models. While cloud models like GPT-4o offer superior accuracy, local models provide privacy, cost control, and offline operation.&lt;/p&gt;
&lt;p&gt;For local inference, I recommend LLaVA as the default for its balance of accuracy and performance. For resource-constrained systems, Qwen2-VL provides excellent results with lower requirements.&lt;/p&gt;
&lt;p&gt;Model specifications and requirements&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ollama.com/library/llava&quot;&gt;LLaVA&lt;/a&gt; (Large Language and Vision Assistant)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;llava:7b&lt;/code&gt; - ~4.5GB download, ~8GB RAM required&lt;/li&gt;
&lt;li&gt;&lt;code&gt;llava:13b&lt;/code&gt; - ~8GB download, ~16GB RAM required&lt;/li&gt;
&lt;li&gt;&lt;code&gt;llava:34b&lt;/code&gt; - ~20GB download, ~40GB RAM required&lt;/li&gt;
&lt;li&gt;Best overall quality for vision tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ollama.com/library/qwen2-vl&quot;&gt;Qwen2-VL&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;qwen2-vl:7b&lt;/code&gt; - ~4GB download, ~6GB RAM required&lt;/li&gt;
&lt;li&gt;Excellent performance with lower resource requirements&lt;/li&gt;
&lt;li&gt;Ideal for less powerful machines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Installation:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Install your chosen model&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;ollama&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; pull llava:latest        &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; or llava:7b, llava:13b, etc.&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;ollama&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; pull qwen2-vl:7b        &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; for resource-constrained systems&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;My MCP Ecosystem&lt;/h2&gt;
&lt;p&gt;Peekaboo is part of a growing collection of MCP servers I’m building:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/claude-code-mcp&quot;&gt;claude-code-mcp&lt;/a&gt;&lt;/strong&gt; - Integrates Claude Code into Cursor for task offloading&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/macos-automator-mcp&quot;&gt;macos-automator-mcp&lt;/a&gt;&lt;/strong&gt; - Run AppleScript and JXA on macOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Terminator&quot;&gt;Terminator&lt;/a&gt;&lt;/strong&gt; - External terminal so agents don’t get stuck on long-running commands&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each serves a specific purpose in building autonomous AI workflows.&lt;/p&gt;
&lt;h2&gt;Technical Architecture&lt;/h2&gt;
&lt;p&gt;Peekaboo combines TypeScript and Swift for the best of both worlds. TypeScript provides excellent &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/modelcontextprotocol/typescript-sdk&quot;&gt;MCP support&lt;/a&gt; and easy distribution via npm, while Swift enables direct access to Apple’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/screencapturekit&quot;&gt;ScreenCaptureKit&lt;/a&gt; for capturing windows without focus changes.&lt;/p&gt;
&lt;p&gt;My initial &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Peekaboo/blob/main/peekaboo.scpt&quot;&gt;AppleScript prototype&lt;/a&gt; had a fatal flaw: it required focus changes to capture windows. The Swift rewrite uses ScreenCaptureKit to access the window manager directly - no focus changes, no user disruption.&lt;/p&gt;
&lt;p&gt;The system uses a Swift CLI that communicates with a Node.js MCP server, supporting both local models and cloud providers with automatic fallback. Built with Swift 6 and the new Swift Testing framework (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/migrating-700-tests-to-swift-testing&quot;&gt;now that I have experience with it!&lt;/a&gt;), Peekaboo delivers fast, non-intrusive screenshot capture with intelligent window matching.&lt;/p&gt;
&lt;p&gt;For detailed testing instructions using the MCP Inspector, see the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Peekaboo#testing--debugging&quot;&gt;Peekaboo README&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Vision: Autonomous Agent Debugging&lt;/h2&gt;
&lt;p&gt;Peekaboo is like one puzzle piece in a larger set of MCPs I’m building to help agents stay in the loop. The goal is simple: if an agent can answer questions by itself, you don’t have to intervene and it can simply continue and debug itself. This is the holy grail for building applications with CI - you want to do everything so the agent can loop and work until what you want is done.&lt;/p&gt;
&lt;p&gt;When your build fails, when your UI doesn’t look right, when something breaks - instead of stopping and asking you “what do you see?”, the agent can take a screenshot, analyze it, and continue fixing the problem autonomously. That’s the power of giving agents their eyes.&lt;/p&gt;
&lt;p&gt;👻 &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.peekaboo.dev/&quot;&gt;Peekaboo MCP&lt;/a&gt; is available now - ⭐ &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Peekaboo&quot;&gt;the repo&lt;/a&gt; if this saves you a debug session!&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Migrating 700+ Tests to Swift Testing: A Real-World Experience</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-06-migrating-700-tests-to-swift-testing-a-real-world-experience/</link>
    <guid isPermaLink="false">urn:aggr:item:d35499c75b03d5b50605d56db79013a48be345a4</guid>
    <atom:link href="https://steipete.me/posts/2025/migrating-700-tests-to-swift-testing/" rel="via" type="text/html"/>
    <pubDate>Fri, 6 Jun 2025 18:00:00 +0000</pubDate>
    <description>How I migrated over 700 tests from XCTest to Swift Testing across two projects, with AI assistance and systematic refinement</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I let Claude Code convert 700+ tests to Swift Testing, watched it fail spectacularly, created an AI-generated playbook, then watched it succeed brilliantly. The difference? Better instructions.&lt;/p&gt;
&lt;p&gt;I’ve been migrating my test suites from XCTest to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/xcode/swift-testing/&quot;&gt;Swift Testing&lt;/a&gt;. Between &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter&quot;&gt;Vibe Meter&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/CodeLooper&quot;&gt;Code Looper&lt;/a&gt;, that’s over 700 tests across 118 files. Here’s what I learned from letting AI help with the conversion and then systematically improving the results using my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95&quot;&gt;Swift Testing playbook&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Initial Attempt&lt;/h2&gt;
&lt;p&gt;Started with the laziest possible prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hey Claude Code, convert all these tests to swift-testing.&lt;br /&gt;
I’ll go make coffee.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The initial results were… technically correct. The tests compiled. They even passed. But looking at the code, it was clear this was just XCTest wearing a Swift Testing costume.&lt;/p&gt;
&lt;p&gt;When AI gives you lemons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XCTestCase&lt;/code&gt; classes became &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/testing/suite&quot;&gt;&lt;code&gt;@Suite&lt;/code&gt;&lt;/a&gt; structs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XCTAssert&lt;/code&gt; calls turned into &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/testing/expect(_:_:sourceLocation:)&quot;&gt;&lt;code&gt;#expect&lt;/code&gt;&lt;/a&gt; statements&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func testFoo()&lt;/code&gt; transformed to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/testing/test&quot;&gt;&lt;code&gt;@Test&lt;/code&gt;&lt;/a&gt; &lt;code&gt;func foo()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But it missed the deeper opportunities that Swift Testing provides. The real work began after my coffee kicked in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full disclosure&lt;/strong&gt;: I actually tried Swift Testing last week but completely messed it up because I linked to the wrong version without understanding it was already integrated into Xcode. Shoutout to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/throwspace/status/1929658866804953371&quot;&gt;Stuart&lt;/a&gt; who nudged me to try again. Sometimes you need that external push to revisit something you wrote off too quickly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Creating a Systematic Approach&lt;/h2&gt;
&lt;p&gt;Instead of manually fixing 700 tests, I did what any reasonable developer would do: I procrastinated by watching WWDC videos. Both &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/videos/play/wwdc2024/10179/&quot;&gt;&lt;em&gt;Meet Swift Testing&lt;/em&gt;&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/videos/play/wwdc2024/10195/&quot;&gt;&lt;em&gt;Go further with Swift Testing&lt;/em&gt;&lt;/a&gt; sessions were eye-opening.&lt;/p&gt;
&lt;p&gt;But here’s where it gets complicated. The WWDC videos referenced outdated APIs that confused my AI agents. Plus, Claude couldn’t access Apple’s documentation because it’s all JavaScript-rendered. I spent hours trying different approaches until I discovered &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.firecrawl.dev/referral?rid=9CG538BE&quot;&gt;Firecrawl&lt;/a&gt; (affiliate link, I need moar credits!), which converted Apple’s entire Swift Testing documentation into a massive Markdown file. (This experience inspired me to build &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/llm-codes-transform-developer-docs&quot;&gt;llm.codes&lt;/a&gt;, where anyone can now generate up-to-date documentation from Apple’s official sources.)&lt;/p&gt;
&lt;p&gt;So I ended up with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95&quot;&gt;two documents&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The complete Swift Testing API documentation from Apple (via &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.firecrawl.dev/referral?rid=9CG538BE&quot;&gt;Firecrawl&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;An actionable playbook with examples from WWDC videos&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I fed both into &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://aistudio.google.com/&quot;&gt;Google’s AI Studio&lt;/a&gt;, asked it to correct the outdated API references, and let Gemini compile everything into a comprehensive &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95&quot;&gt;Swift Testing resource&lt;/a&gt; with both the API documentation and actionable migration patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why go through all this effort?&lt;/strong&gt; Swift Testing is new and underrepresented in open source compared to mature ecosystems like TypeScript/React where AI has absorbed countless examples. For emerging technologies, you need to explicitly provide the knowledge that doesn’t exist in the training data yet.&lt;/p&gt;
&lt;p&gt;The key insight: AI needs concrete patterns and examples, not just documentation. This playbook became my guide for teaching Claude Code how to write &lt;em&gt;idiomatic&lt;/em&gt; Swift Testing code, providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migration patterns with before/after examples&lt;/li&gt;
&lt;li&gt;Best practices for each Swift Testing feature&lt;/li&gt;
&lt;li&gt;Common pitfalls and how to avoid them&lt;/li&gt;
&lt;li&gt;Specific guidance on when to use each feature&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Round Two: AI Redemption Arc&lt;/h2&gt;
&lt;p&gt;With the playbook in hand, I gave Claude Code new instructions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Read &lt;code&gt;swift-testing-playbook.md&lt;/code&gt; and improve &amp;amp; refactor the tests. Periodically stop, compile, fix any build issues, commit, ensure everything is green locally and on CI, then continue until perfection.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Plot twist&lt;/strong&gt;: The first time I tried this, Claude got creative and started implementing &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/CodeLooper/pull/8/commits/d1bb7ba75dfefc821d24d5cfff6c785cd92f9079&quot;&gt;new test patterns from the playbook&lt;/a&gt; instead of converting existing tests. I had to clarify:&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/claude-being-dumb.png&quot; alt=&quot;Claude getting carried away with implementing test patterns instead of converting existing tests&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Read &lt;code&gt;swift-testing-playbook.md&lt;/code&gt; and &lt;code&gt;swift-testing-api.md&lt;/code&gt; and refactor the &lt;strong&gt;existing&lt;/strong&gt; tests in this codebase to use Swift Testing patterns.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Lesson learned: Be very specific with AI instructions. “Improve tests using these patterns” can mean “create new tests” or “convert existing tests.” The difference matters when you have existing test coverage to preserve.&lt;/p&gt;
&lt;p&gt;This iterative approach revealed several patterns worth sharing:&lt;/p&gt;
&lt;h3&gt;1. Nested Suites Bring Order to Chaos&lt;/h3&gt;
&lt;p&gt;The biggest transformation came from consolidating scattered test files into organized hierarchies. Vibe Meter’s AuthenticationTokenManager tests showed the most dramatic improvement:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;// Before: 4 separate test files
VibeMeterTests/
├── AuthenticationTokenManagerCoreTests.swift
├── AuthenticationTokenManagerCookieTests.swift  
├── AuthenticationTokenManagerEdgeCasesTests.swift
└── AuthenticationTokenManagerTests.swift
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To this beauty:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;@Suite(&amp;quot;Authentication Token Manager Tests&amp;quot;, .tags(.authentication, .unit))
@MainActor
struct AuthenticationTokenManagerTests {
    // MARK: - Core Functionality
    @Suite(&amp;quot;Core Functionality&amp;quot;, .tags(.fast))
    struct Core {
        let mockManager: MockAuthenticationTokenManager
        
        init() {
            mockManager = MockAuthenticationTokenManager()
        }
        
        @Test(&amp;quot;save token success&amp;quot;, .tags(.critical, .requiresKeychain))
        func saveTokenSuccess() async throws {
            // Core authentication tests...
        }
    }
    
    // MARK: - Cookie Management
    @Suite(&amp;quot;Cookie Management&amp;quot;)
    struct CookieManagement {
        @Test(&amp;quot;cookie extraction from headers&amp;quot;)
        func cookieExtractionFromHeaders() async throws {
            // Cookie-related tests...
        }
    }
    
    // MARK: - Edge Cases
    @Suite(&amp;quot;Edge Cases&amp;quot;, .tags(.edgeCase, .fast))
    struct EdgeCases {
        @Test(&amp;quot;handles concurrent token updates&amp;quot;)
        func handlesConcurrentTokenUpdates() async throws {
            // Edge case tests...
        }
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;75% fewer files, with logical organization that mirrors the actual functionality. Each nested suite can have its own setup, and the test navigator in Xcode finally makes sense.&lt;/p&gt;
&lt;h3&gt;2. Parameterized Tests Eliminate Copy-Paste Syndrome&lt;/h3&gt;
&lt;p&gt;Remember writing the same test five times with different values? Vibe Meter’s currency conversion tests showcase Swift Testing’s superior parameterization:&lt;/p&gt;
&lt;p&gt;View Currency Conversion Test Implementation&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Before: Individual test methods for each scenario
func testConvert_SmallAmount() { /* test code */ }
func testConvert_LargeAmount() { /* test code */ }
func testConvert_NegativeAmount() { /* test code */ }
func testConvert_ZeroAmount() { /* test code */ }
func testConvert_PrecisionAmount() { /* test code */ }
// ... and many more

// After: Sophisticated parameterized testing
@Suite(&amp;quot;Currency Conversion Tests&amp;quot;, .tags(.currency, .unit))
struct CurrencyConversionTests {
    struct ConversionTestCase: Sendable, CustomTestStringConvertible {
        let amount: Double
        let rate: Double
        let expected: Double
        let description: String
        
        var testDescription: String {
            &amp;quot;$\(amount) × \(rate) → $\(expected) (\(description))&amp;quot;
        }
    }
    
    static let conversionTestCases: [ConversionTestCase] = [
        ConversionTestCase(100.0, rate: 0.85, expected: 85.0, &amp;quot;USD to EUR conversion&amp;quot;),
        ConversionTestCase(0.0, rate: 0.85, expected: 0.0, &amp;quot;zero amount conversion&amp;quot;),
        ConversionTestCase(100.0, rate: 1.0, expected: 100.0, &amp;quot;same currency conversion&amp;quot;),
        ConversionTestCase(-100.0, rate: 0.85, expected: -85.0, &amp;quot;negative amount conversion&amp;quot;),
        ConversionTestCase(1_000_000.0, rate: 0.85, expected: 850_000.0, &amp;quot;large amount conversion&amp;quot;),
        ConversionTestCase(0.01, rate: 0.85, expected: 0.0085, &amp;quot;small amount conversion&amp;quot;),
        ConversionTestCase(999.99, rate: 1.2345, expected: 1234.488, &amp;quot;precision conversion&amp;quot;),
    ]
    
    @Test(&amp;quot;Currency conversion calculations&amp;quot;, .tags(.critical), 
          arguments: CurrencyConversionTests.conversionTestCases)
    func conversionCalculations(testCase: ConversionTestCase) async {
        let result = await MainActor.run {
            CurrencyConversionHelper.convert(amount: testCase.amount, rate: testCase.rate)
        }
        
        let tolerance = testCase.expected.magnitude &amp;lt; 1.0 ? 0.0001 : 0.01
        #expect(result.isApproximatelyEqual(to: testCase.expected, tolerance: tolerance))
    }
    
    @Test(&amp;quot;Invalid rate handling&amp;quot;, .tags(.edgeCase), 
          arguments: [nil, 0.0, -1.0, .infinity, .nan] as [Double?])
    func invalidRateHandling(invalidRate: Double?) async {
        let result = await MainActor.run {
            CurrencyConversionHelper.convert(amount: 100.0, rate: invalidRate)
        }
        #expect(result == 100.0) // Should return original amount for invalid rates
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The beauty? &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/testing/customteststringconvertible&quot;&gt;&lt;code&gt;CustomTestStringConvertible&lt;/code&gt;&lt;/a&gt; makes each test case self-documenting in the test navigator with descriptions like “$100.0 × 0.85 → $85.0 (USD to EUR conversion)” instead of generic object names. Test failures become instantly readable, and we can test edge cases (nil, infinity, NaN) in a single elegant test.&lt;/p&gt;
&lt;h3&gt;3. Instance Isolation Simplifies State Management&lt;/h3&gt;
&lt;p&gt;Swift Testing creates a fresh instance for each test, eliminating shared state issues:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;@Suite struct DatabaseTests {
    let db: TestDatabase
    let tempDir: URL
    
    init() throws {
        tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: tempDir)
        db = TestDatabase(path: tempDir)
    }
    
    deinit {
        try? FileManager.default.removeItem(at: tempDir)
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No more worrying about test order or cleanup between tests. Each test gets its own clean instance.&lt;/p&gt;
&lt;h3&gt;4. Better Error Handling and Assertions&lt;/h3&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/testing/&quot;&gt;Swift Testing&lt;/a&gt; provides two types of assertions that behave very differently:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// #expect: Soft assertion - continues test on failure
#expect(user.email == &amp;quot;test@example.com&amp;quot;)
#expect(user.isActive == true)
// ↑ Both assertions run even if first one fails

// #require: Hard assertion - stops test immediately on failure
let config = try #require(loadConfiguration())
// ↑ Test stops here if config is nil, preventing crashes below
let apiKey = try #require(config.apiKey)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Error handling is also more expressive than XCTest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Validate specific error types
#expect(throws: NetworkError.self) {
    try await api.fetchWithoutAuth()
}

// Ensure no errors
#expect(throws: Never.self) {
    try parseValidJSON(data)
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Time Limits Prevent CI Hangs&lt;/h3&gt;
&lt;p&gt;Performance tests were scattered across multiple files with no protection against runaway execution:&lt;/p&gt;
&lt;p&gt;View Performance Test Implementation&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Before: Manual timing with XCTest
func testLogging_Performance() {
    let iterations = 10_000
    let testMessage = &amp;quot;Performance test message&amp;quot;
    
    measure {
        for i in 0..&amp;lt;iterations {
            LoggingService.info(&amp;quot;\(testMessage) \(i)&amp;quot;)
        }
    }
}

func testCurrencyConversionPerformance() {
    self.measure {
        for _ in 0..&amp;lt;1000 {
            _ = CurrencyHelper.convert(100.0, rate: 0.85)
        }
    }
}

// After: Clean, declarative performance tests
@Suite(&amp;quot;Performance Benchmarks&amp;quot;, .tags(.performance))
struct PerformanceBenchmarks {
    @Test(&amp;quot;Bulk currency conversion performance&amp;quot;, .timeLimit(.minutes(1)))
    @MainActor
    func bulkCurrencyConversionPerformance() {
        // Test configuration
        let amounts = Array(stride(from: 0.01, through: 10_000.0, by: 0.01))
        let exchangeRates = [&amp;quot;EUR&amp;quot;: 0.92, &amp;quot;GBP&amp;quot;: 0.82, &amp;quot;JPY&amp;quot;: 110.0, &amp;quot;AUD&amp;quot;: 1.35, &amp;quot;CAD&amp;quot;: 1.25]
        
        // Performance test
        let startTime = Date()
        for amount in amounts {
            for (_, rate) in exchangeRates {
                _ = CurrencyConversionHelper.convert(amount: amount, rate: rate)
            }
        }
        let duration = Date().timeIntervalSince(startTime)
        
        // Verify performance
        print(&amp;quot;Converted \(amounts.count * exchangeRates.count) values in \(duration)s&amp;quot;)
        #expect(duration &amp;lt; 10.0) // Framework enforces the 1-minute limit
    }
    
    @Test(&amp;quot;Logging throughput&amp;quot;, .timeLimit(.seconds(30)))
    func loggingThroughput() async {
        let iterations = 100_000
        
        // No manual measurement needed - framework handles it
        for i in 0..&amp;lt;iterations {
            LoggingService.debug(&amp;quot;Test message \(i)&amp;quot;)
        }
        
        // Test will fail if it exceeds 30 seconds
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents runaway tests from blocking CI pipelines. Something I’ve dealt with too many times in XCTest.&lt;/p&gt;
&lt;h2&gt;Beyond Basic Conversion: Real Improvements&lt;/h2&gt;
&lt;p&gt;The mechanical migration was just the beginning. Here’s where Swift Testing really shines:&lt;/p&gt;
&lt;h3&gt;Meaningful Error Testing with #expect(throws:)&lt;/h3&gt;
&lt;p&gt;Stop settling for “it didn’t crash” assertions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Before: Wishful thinking
do {
    try await manager.startListener()
} catch {
    #expect(error != nil) // Well, yes... that&amp;#39;s why we&amp;#39;re in catch
}

// After: Actual validation
#expect(throws: URLError.self) {
    try await api.fetchWithoutAuth()
}

// Even better: Test the specific error properties
do {
    try await api.fetchWithoutAuth()
    Issue.record(&amp;quot;Expected URLError to be thrown&amp;quot;)
} catch let error as URLError {
    #expect(error.code == .notConnectedToInternet)
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Memory Leak Detection Built Right In&lt;/h3&gt;
&lt;p&gt;Swift Testing’s instance isolation makes leak detection elegant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;@Test(&amp;quot;ThreadSafeBox instances are properly deallocated&amp;quot;)
func threadSafeBoxMemoryLeaks() async throws {
    weak var weakBox: ThreadSafeBox&amp;lt;String&amp;gt;?
    
    do {
        let box = ThreadSafeBox(&amp;quot;test-value&amp;quot;)
        weakBox = box
        #expect(weakBox != nil, &amp;quot;Box should be alive within scope&amp;quot;)
    }
    
    await Task.yield() // Allow deallocation
    #expect(weakBox == nil, &amp;quot;Box should be deallocated after scope ends&amp;quot;)
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Descriptive Test Names That Tell Stories&lt;/h3&gt;
&lt;p&gt;No more cryptic function names:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Before: What does this even test?
@Test(&amp;quot;windowControllerManagement&amp;quot;)

// After: Crystal clear intent
@Test(&amp;quot;Window controller management handles creation and lifecycle&amp;quot;)
@Test(&amp;quot;Position saving and restoration persists window states&amp;quot;)
@Test(&amp;quot;AppleScript support methods provide automation capabilities&amp;quot;)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advanced Confirmation Patterns&lt;/h3&gt;
&lt;p&gt;Swift Testing’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/testing/confirmation(_:expectedcount:sourcelocation:_:)&quot;&gt;&lt;code&gt;confirmation()&lt;/code&gt;&lt;/a&gt; handles complex async scenarios elegantly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;@Test(&amp;quot;Multi-step debouncer lifecycle&amp;quot;)
func multiStepDebouncerLifecycle() async throws {
    try await confirmation(&amp;quot;Complete lifecycle&amp;quot;, expectedCount: 3) { confirmation in
        // Step 1: Initial call
        debouncer.call { confirmation() }
        try await Task.sleep(for: .milliseconds(70))
        
        // Step 2: Second call after first completes
        debouncer.call { confirmation() }
        try await Task.sleep(for: .milliseconds(70))
        
        // Step 3: Final verification
        confirmation()
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Taming Flaky Tests with withKnownIssue&lt;/h3&gt;
&lt;p&gt;Every codebase has those intermittent failures. Swift Testing provides a civilized way to handle them with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/testing/withknownissue(_:isintermittent:sourcelocation:_:)&quot;&gt;&lt;code&gt;withKnownIssue&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;@Test(&amp;quot;External API availability check&amp;quot;)
func externalAPITest() async throws {
    await withKnownIssue(&amp;quot;External API may be temporarily unavailable&amp;quot;, isIntermittent: true) {
        let url = URL(string: &amp;quot;https://api.exchangerate-api.com/v4/latest/USD&amp;quot;)!
        let (_, response) = try await URLSession.shared.data(from: url)
        let httpResponse = response as! HTTPURLResponse
        #expect(httpResponse.statusCode == 200, &amp;quot;Exchange rate API should be available&amp;quot;)
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Results&lt;/h2&gt;
&lt;p&gt;Looking at the final pull requests (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/pull/28&quot;&gt;Vibe Meter PR #28&lt;/a&gt;, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/CodeLooper/pull/8&quot;&gt;Code Looper PR #8&lt;/a&gt;), the transformation is dramatic:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Combined Results:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;46% fewer test files&lt;/strong&gt; (91 → 49 files) through intelligent consolidation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;+3,258 lines of enhanced test code&lt;/strong&gt; across both projects
&lt;ul&gt;
&lt;li&gt;Vibe Meter: 61 → 23 files, +1,006 lines&lt;/li&gt;
&lt;li&gt;Code Looper: 30 → 26 files, +2,252 lines&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero test duplication&lt;/strong&gt; thanks to parameterized tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hierarchical organization&lt;/strong&gt; that actually makes sense in Xcode’s navigator&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bulletproof error handling&lt;/strong&gt; with specific exception types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI that doesn’t hang&lt;/strong&gt; with proper timeouts on every async test&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why more lines of code?&lt;/strong&gt; Swift Testing prioritizes maintainability over brevity. Here’s a typical transformation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;// Before: XCTest (3 lines)
func testCurrencyConversion() {
    XCTAssertEqual(convert(100, rate: 0.85), 85.0, accuracy: 0.01)
}

// After: Swift Testing (5 lines)
@Test(&amp;quot;Currency conversion with EUR rate&amp;quot;, .tags(.currency))
func currencyConversionEUR() {
    let result = convert(amount: 100.0, rate: 0.85)
    #expect(result.isApproximatelyEqual(to: 85.0, tolerance: 0.01))
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The increase comes from descriptive test names, explicit tagging, better parameter naming, and structured test case types for parameterized tests.&lt;/p&gt;
&lt;p&gt;This represents quality improvement, not bloat. Moving from cryptic but compact tests to self-documenting, maintainable test suites.&lt;/p&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AI needs guidance&lt;/strong&gt;: The initial blind conversion was just the starting point. The real value came from providing structured patterns through the playbook.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Iterative refinement works&lt;/strong&gt;: The approach of compile-test-commit-repeat caught issues early and made debugging easier.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Swift Testing encourages better patterns&lt;/strong&gt;: Features like parameterized tests and instance isolation naturally lead to cleaner test design.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Migration reveals test quality issues&lt;/strong&gt;: This wasn’t just a syntax conversion. It was an opportunity to improve test architecture.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Additional AI Resources for Swift Testing&lt;/h2&gt;
&lt;p&gt;For Cursor users, there’s the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.cursor.com/context/@-symbols/@-docs&quot;&gt;@Docs&lt;/a&gt; context which &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://raw.githubusercontent.com/getcursor/crawler/main/docs.jsonl&quot;&gt;includes&lt;/a&gt; the Apple Documentation as a choice.&lt;/p&gt;
&lt;p&gt;There’s also the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://context7.com/swiftlang/swift-testing&quot;&gt;Context 7 MCP&lt;/a&gt; that can be instructed to fetch Apple’s docs for testing, however this only includes code snippets and no explanation. (Enes mentioned that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/enesakar/status/1931296179502927935&quot;&gt;this is on purpose&lt;/a&gt;, though I got great results with explanatory text)&lt;/p&gt;
&lt;p&gt;The combination of AI assistance and systematic refinement made this large-scale migration manageable. While the initial AI conversion provided a foundation, the real value came from applying Swift Testing’s features thoughtfully to create a more maintainable test suite.&lt;/p&gt;
&lt;h2&gt;Running Tests Faster with Filters&lt;/h2&gt;
&lt;p&gt;One more tip that significantly speeds up the development workflow: use &lt;code&gt;swift test --filter&lt;/code&gt; to run specific tests instead of the entire suite. This is especially useful when you’re iterating on a particular feature:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Run a single test suite by name&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;swift&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; test&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;filter&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;FooTests&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Run tests with specific tags&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;swift&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; test&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;filter-tag&lt;/span&gt; fast&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;swift&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; test&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;filter-tag&lt;/span&gt; currency&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Combine multiple tags (runs tests with ALL specified tags)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;swift&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; test&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;filter-tag&lt;/span&gt; unit&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;filter-tag&lt;/span&gt; fast&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Skip tests with certain tags&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;swift&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; test&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;skip-tag&lt;/span&gt; slow&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;swift&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; test&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;skip-tag&lt;/span&gt; requiresNetwork&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The performance difference is dramatic when working on a large codebase. Instead of waiting for 700+ tests to run, you can get feedback in seconds by filtering to just the tests you’re working on. Combined with Swift Testing’s parallel execution, this makes the test-driven development cycle incredibly fast.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; - If you’re still manually converting &lt;code&gt;XCTestExpectation&lt;/code&gt;, stop. &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer&quot;&gt;Make AI do it&lt;/a&gt;. Just give it &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95&quot;&gt;better instructions&lt;/a&gt; than I did.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Commanding Your Claude Code Army</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-05-commanding-your-claude-code-army/</link>
    <guid isPermaLink="false">urn:aggr:item:6b88db97dacb072f477af994b3ee27864767b0a2</guid>
    <atom:link href="https://steipete.me/posts/2025/commanding-your-claude-code-army/" rel="via" type="text/html"/>
    <pubDate>Thu, 5 Jun 2025 09:00:00 +0000</pubDate>
    <description>How a simple terminal trick helps me manage multiple Claude Code instances without losing my mind (or my terminal tabs)</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/commanding-your-claude-code-army/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: There’s a better solution now! Check out &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/command-your-claude-code-army-reloaded/&quot;&gt;Command Your Claude Code Army, Reloaded&lt;/a&gt; for VibeTunnel integration that gives Claude automatic title management.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Running multiple Claude Code instances? Here’s a ZSH trick to keep your terminal titles organized and your sanity intact.&lt;/p&gt;
&lt;p&gt;I’ll admit it - I’ve become a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/claude-code&quot;&gt;Claude Code&lt;/a&gt; power user. At any given moment, I have at least three instances running: one for writing blog posts, another fixing that “quick” bug that’s now in hour three, and a third doing something I definitely forgot about. My terminal looks like a Claude Code convention.&lt;/p&gt;
&lt;h2&gt;The Problem: Too Many Claudes, Not Enough Context&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ghostty.org/&quot;&gt;Ghostty&lt;/a&gt; (yes, with two T’s - it’s my favorite terminal) only shows the process name in the title. When you have six tabs all saying “claude”, finding the right one becomes a game of terminal roulette.&lt;/p&gt;
&lt;p&gt;This is especially painful when you’re running Claude with the &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; flag (yes, I live dangerously - see my post about &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/claude-code-is-my-computer/&quot;&gt;Claude Code being my computer&lt;/a&gt;). The last thing you want is to accidentally run the wrong command in the wrong directory with full system permissions.&lt;/p&gt;
&lt;p&gt;To make matters worse, Claude Code has a habit of changing the terminal title to whatever it feels like - usually something unhelpful like “Running command…” or just “claude”. It’s like having a helpful assistant who keeps reorganizing your desk while you’re trying to work.&lt;/p&gt;
&lt;h2&gt;The Solution: Terminal Title Magic&lt;/h2&gt;
&lt;p&gt;Here’s my approach - a clean ZSH setup that keeps terminal management code separate from my main configuration. First, I add one line to my &lt;code&gt;~/.zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Source Claude wrapper with dynamic terminal title&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-support syntax-function syntax-test syntax-begin syntax-shell&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;-&lt;/span&gt;f&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-tilde&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-tilde syntax-shell&quot;&gt;~&lt;/span&gt;&lt;/span&gt;/.config/zsh/claude-wrapper.zsh &lt;span class=&quot;syntax-support syntax-function syntax-test syntax-end syntax-shell&quot;&gt;]&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-and syntax-shell&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-source syntax-shell&quot;&gt;source&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-tilde&quot;&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-tilde syntax-shell&quot;&gt;~&lt;/span&gt;&lt;/span&gt;/.config/zsh/claude-wrapper.zsh&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I create &lt;code&gt;~/.config/zsh/claude-wrapper.zsh&lt;/code&gt; with the actual implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;!/usr/bin/env zsh&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Simple, elegant terminal title management for Claude&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;
&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Set terminal title&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-shell&quot;&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-shell&quot;&gt;_set_title&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt; 
    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;print&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;Pn&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;\e]2;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;1&lt;/span&gt;&lt;/span&gt;\a&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;
&lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Claude wrapper with custom terminal title&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-shell&quot;&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-shell&quot;&gt;cly&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;    &lt;span class=&quot;syntax-storage syntax-modifier syntax-shell&quot;&gt;local&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;folder&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-shell&quot;&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;PWD&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-substring syntax-begin syntax-shell&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;  &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Just the current folder name&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Set title to show we&amp;#39;re running Claude&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;_set_title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;folder&lt;/span&gt;&lt;/span&gt; — Claude&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Start a background process to continuously reset the title&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; (prevents Claude from changing it)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-punctuation syntax-definition syntax-compound syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;syntax-keyword syntax-control syntax-while syntax-shell&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-continue syntax-shell&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-do syntax-shell&quot;&gt;do&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;_set_title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;folder&lt;/span&gt;&lt;/span&gt; — Claude&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sleep&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; 0.5&lt;/span&gt;
        &lt;span class=&quot;syntax-keyword syntax-control syntax-done syntax-shell&quot;&gt;done&lt;/span&gt;
    &lt;span class=&quot;syntax-punctuation syntax-definition syntax-compound syntax-end syntax-shell&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-job syntax-shell&quot;&gt;&amp;amp;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;    &lt;span class=&quot;syntax-storage syntax-modifier syntax-shell&quot;&gt;local&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;title_pid&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-shell&quot;&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-shell&quot;&gt;!&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Run Claude with dangerous permissions&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;&lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;HOME&lt;/span&gt;&lt;/span&gt;/.claude/local/claude&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;dangerously-skip-permissions&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-shell&quot;&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;    &lt;span class=&quot;syntax-storage syntax-modifier syntax-shell&quot;&gt;local&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;exit_code&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-shell&quot;&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-language syntax-shell&quot;&gt;?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Kill the background title setter&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-kill syntax-shell&quot;&gt;kill&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;title_pid&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-file-descriptor syntax-shell&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-wait syntax-shell&quot;&gt;wait&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;title_pid&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-file-descriptor syntax-shell&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null  &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Clean up zombie process&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;    
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Restore normal title&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;_set_title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;%~&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    
    &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Return the original exit code&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-return syntax-shell&quot;&gt;return&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;exit_code&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Update terminal title before each prompt (using proper ZSH hooks)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function syntax-shell&quot;&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-function syntax-shell&quot;&gt;_claude_precmd&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;_set_title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;%~&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Add our precmd function to the array (doesn&amp;#39;t overwrite existing hooks)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-begin syntax-shell&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-double-brace syntax-begin syntax-shell&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;-&lt;/span&gt;z&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;precmd_functions&lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-begin syntax-shell&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-parens syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell&quot;&gt;)&lt;/span&gt;&lt;/span&gt;_claude_precmd&lt;span class=&quot;syntax-punctuation syntax-section syntax-braces syntax-end syntax-shell&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-support syntax-function syntax-double-brace syntax-end syntax-shell&quot;&gt;]]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-continue syntax-shell&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-then syntax-shell&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;precmd_functions&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;_claude_precmd&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-end syntax-shell&quot;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I run &lt;code&gt;cly&lt;/code&gt;, my terminal title changes from &lt;code&gt;~/Projects/blog&lt;/code&gt; to &lt;code&gt;~/Projects/blog — Claude&lt;/code&gt;. Revolutionary? No. Life-changing? Absolutely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Hot tip: Just ask Claude to read this blog post and set it up for you! (in yolo mode, of course)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now if you’ll excuse me, I need to check on my Claude instances. I think one of them is refactoring my entire codebase without asking. Again.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Code Signing and Notarization: Sparkle and Tears</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-05-code-signing-and-notarization-sparkle-and-tears/</link>
    <guid isPermaLink="false">urn:aggr:item:76d5f33058c84a76be425a01280acb64b1a1a4d7</guid>
    <atom:link href="https://steipete.me/posts/2025/code-signing-and-notarization-sparkle-and-tears/" rel="via" type="text/html"/>
    <pubDate>Thu, 5 Jun 2025 07:00:00 +0000</pubDate>
    <description>My brutal journey implementing Sparkle auto-updates in sandboxed macOS apps - from 40 failed releases to enlightenment.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/header.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Or: How I Learned to Stop Worrying and Love the XPC Services&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you’ve ever tried to implement automatic updates in a sandboxed macOS app using Sparkle, you know it can feel like trying to solve a Rubik’s cube while wearing oven mitts. After creating way too many beta releases and spending countless hours debugging cryptic authorization errors, I finally cracked the code. Here’s my step-by-step guide to code signing, notarization, and Sparkle auto-updates in sandboxed macOS apps.&lt;/p&gt;
&lt;h2&gt;The Setup: Vibe Meter Meets Sparkle&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibemeter.ai&quot;&gt;Vibe Meter&lt;/a&gt; is a sandboxed macOS menu bar app that tracks AI service spending. When I decided to add automatic updates using Sparkle 2.x, I thought it would be straightforward. After all, Sparkle is the de facto standard for macOS app updates, right?&lt;/p&gt;
&lt;p&gt;Oh, sweet summer child.&lt;/p&gt;
&lt;h2&gt;Act 1: The Mysterious Authorization Failure&lt;/h2&gt;
&lt;p&gt;I’d already mastered code signing and notarization (its own nightmare), so I just told Claude: “Look at this repository and do what we did there.” It nailed the signing and notarization process.&lt;/p&gt;
&lt;p&gt;My first attempts seemed promising. The app built, signed, and notarized successfully. But when users tried to update, they were greeted with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;Error: Failed to gain authorization required to update target
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This error is Sparkle’s polite way of saying “I can’t talk to my XPC services, and I have no idea why.”&lt;/p&gt;
&lt;h2&gt;Act 2: The Entitlements Enigma&lt;/h2&gt;
&lt;p&gt;After digging through &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sparkle-project.org/documentation/sandboxing/&quot;&gt;Sparkle’s documentation&lt;/a&gt; and Console logs, I discovered my first issue: missing mach-lookup entitlements. In a sandboxed app, Sparkle uses XPC services to perform privileged operations, and these services need special permissions to communicate.&lt;/p&gt;
&lt;h3&gt;The Missing Piece&lt;/h3&gt;
&lt;p&gt;My entitlements file was missing a critical entry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.apple.security.temporary-exception.mach-lookup.global-name&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.steipete.vibemeter-spks&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.steipete.vibemeter-spki&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But here’s the kicker - I initially only added &lt;code&gt;-spks&lt;/code&gt;, thinking it stood for “Sparkle Server.” Turns out, you need BOTH:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-spks&lt;/code&gt;: Sparkle Server (for the InstallerLauncher.xpc service)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-spki&lt;/code&gt;: Sparkle Installer (for the Installer.xpc service)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Missing either one results in the dreaded authorization error.&lt;/p&gt;
&lt;h2&gt;Act 3: The Code Signing Circus&lt;/h2&gt;
&lt;p&gt;Next came the code signing adventures. My notarization script was doing what seemed logical:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;deep&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;force&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;sign&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Developer ID&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; VibeMeter.app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But Sparkle’s documentation specifically warns against using &lt;code&gt;--deep&lt;/code&gt;. Why? Because it can mess up the XPC services’ signatures. Instead, you need to sign components in a specific order.&lt;/p&gt;
&lt;p&gt;Here’s the correct approach from my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh&quot;&gt;codesign script&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Sign XPC services first&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;f&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;s&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;SIGN_IDENTITY&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;o&lt;/span&gt; runtime &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;    &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Preserve entitlements for Downloader.xpc (Sparkle 2.6+)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;f&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;s&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;SIGN_IDENTITY&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;o&lt;/span&gt; runtime&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;preserve-metadata&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-option syntax-shell&quot;&gt;=&lt;/span&gt;entitlements &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;    &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Sign other Sparkle components&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;f&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;s&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;SIGN_IDENTITY&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;o&lt;/span&gt; runtime &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;    &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Then sign the framework&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;f&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;s&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;SIGN_IDENTITY&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;o&lt;/span&gt; runtime &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;    &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;/Contents/Frameworks/Sparkle.framework&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Finally, sign the app WITHOUT --deep&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;codesign&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;force&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;sign&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;SIGN_IDENTITY&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;entitlements&lt;/span&gt; VibeMeter.entitlements &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;    --&lt;/span&gt;options&lt;/span&gt; runtime VibeMeter.app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Act 4: The Bundle ID Bamboozle&lt;/h2&gt;
&lt;p&gt;At one point, I thought I was being clever (or rather, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/claude-code-is-my-computer/&quot;&gt;Claude Code&lt;/a&gt; thought…) by trying to manually modify the XPC services’ bundle identifiers. Big mistake.&lt;/p&gt;
&lt;p&gt;For sandboxed apps, Sparkle requires specific bundle ID suffixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;-spks&lt;/code&gt; suffix for the InstallerLauncher service&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;-spki&lt;/code&gt; suffix for the Installer service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The framework expects these exact suffixes. Use anything else, and you’ll get cryptic XPC connection errors that will make you question your career choices.&lt;/p&gt;
&lt;h2&gt;Act 5: The Build Number Blues&lt;/h2&gt;
&lt;p&gt;Even after fixing all the sandboxing issues, I hit another snag. Users were seeing “You’re up to date!” when updates were clearly available. The culprit? My appcast&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fnref-1&quot; data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt; generation script was defaulting build numbers to “1”.&lt;/p&gt;
&lt;p&gt;Sparkle uses build numbers (CFBundleVersion), not version strings, to determine if an update is available. If your build numbers don’t increment, Sparkle thinks there’s nothing new.&lt;/p&gt;
&lt;p&gt;My &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh&quot;&gt;appcast generation script&lt;/a&gt; now properly handles this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Extract build number from Info.plist&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell&quot;&gt;BUILD_NUMBER&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-assignment syntax-shell&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;syntax-string syntax-unquoted syntax-shell&quot;&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-command syntax-parens syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;plutil&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;extract&lt;/span&gt; CFBundleVersion raw &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;INFO_PLIST&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Ensure build numbers increment correctly&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-begin syntax-shell&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-double-brace syntax-begin syntax-shell&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;BUILD_NUMBER&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;-&lt;/span&gt;le&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;PREVIOUS_BUILD_NUMBER&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-support syntax-function syntax-double-brace syntax-end syntax-shell&quot;&gt;]]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-continue syntax-shell&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-then syntax-shell&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-echo syntax-shell&quot;&gt;echo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;Error: Build number must be greater than previous release&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-support syntax-function syntax-exit syntax-shell&quot;&gt;exit&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; 1&lt;/span&gt;
&lt;span class=&quot;syntax-keyword syntax-control syntax-if syntax-end syntax-shell&quot;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Grand Finale: It Works!&lt;/h2&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/sparkle.png&quot; alt=&quot;Sparkle update dialog working perfectly&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;After two days of intense debugging, I finally had a working setup. My complete automation pipeline is now rock-solid, with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/tree/main/scripts&quot;&gt;comprehensive scripts&lt;/a&gt; that handle every aspect of the process.&lt;/p&gt;
&lt;h3&gt;The Magic Recipe&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Entitlements&lt;/strong&gt;: Include BOTH &lt;code&gt;-spks&lt;/code&gt; and &lt;code&gt;-spki&lt;/code&gt; mach-lookup exceptions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bundle IDs&lt;/strong&gt;: Never change Sparkle’s XPC service bundle IDs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Signing&lt;/strong&gt;: Sign XPC services individually, never use &lt;code&gt;--deep&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Numbers&lt;/strong&gt;: Always increment them, and verify your appcast&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Info.plist&lt;/strong&gt;: Set &lt;code&gt;SUEnableInstallerLauncherService = true&lt;/code&gt; and &lt;code&gt;SUEnableDownloaderService = false&lt;/code&gt;&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-2&quot; id=&quot;fnref-2&quot; data-footnote-ref&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Working Configuration&lt;/h3&gt;
&lt;p&gt;The XPC services themselves keep their original Sparkle bundle IDs (&lt;code&gt;org.sparkle-project.InstallerLauncher&lt;/code&gt; and &lt;code&gt;org.sparkle-project.Downloader&lt;/code&gt;). What we configure in the entitlements are the mach-lookup communication channels:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;XML&quot;&gt;&lt;span class=&quot;syntax-text syntax-xml&quot;&gt;&lt;span class=&quot;syntax-comment syntax-block syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-xml&quot;&gt;&amp;lt;!--&lt;/span&gt; VibeMeter.entitlements &lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-end syntax-xml&quot;&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.apple.security.app-sandbox&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.apple.security.network.client&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.apple.security.temporary-exception.mach-lookup.global-name&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.steipete.vibemeter-spks&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;com.steipete.vibemeter-spki&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-tag syntax-xml&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;syntax-entity syntax-name syntax-tag syntax-localname syntax-xml&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Scripts That Saved My Sanity&lt;/h2&gt;
&lt;p&gt;My complete build pipeline consists of several specialized scripts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/preflight-check.sh&quot;&gt;preflight-check.sh&lt;/a&gt;&lt;/strong&gt;: Validates environment and prerequisites&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh&quot;&gt;codesign-app.sh&lt;/a&gt;&lt;/strong&gt;: Handles the complex code signing process&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh&quot;&gt;notarize-app.sh&lt;/a&gt;&lt;/strong&gt;: Manages notarization and stapling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/create-dmg.sh&quot;&gt;create-dmg.sh&lt;/a&gt;&lt;/strong&gt;: Creates and signs distribution DMGs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh&quot;&gt;generate-appcast.sh&lt;/a&gt;&lt;/strong&gt;: Generates Sparkle appcast files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh&quot;&gt;release.sh&lt;/a&gt;&lt;/strong&gt;: Orchestrates the entire release process&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Look at this beauty! Now even Claude can do releases without messing up 🎉 I just tell it “Create a new beta release, see release.md” and it handles everything.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Now create the release&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./scripts/release.sh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; stable        &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; For stable 1.1.0&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; OR&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./scripts/release.sh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; beta 1        &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; For 1.1.0-beta.1&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Use the version script to bump versions&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./scripts/version.sh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;minor       &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; 1.0.0 -&amp;gt; 1.1.0&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./scripts/version.sh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;major       &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; 1.0.0 -&amp;gt; 2.0.0&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;./scripts/version.sh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;set&lt;/span&gt; 1.1.0   &lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Set specific version&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything happens automatically - from building to GitHub release creation. These scripts are tuned for &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://tuist.dev/&quot;&gt;Tuist&lt;/a&gt; and can easily be adapted for simpler setups.&lt;/p&gt;
&lt;h2&gt;The Notarization Nightmare&lt;/h2&gt;
&lt;p&gt;Apple’s notarization process adds another layer of complexity. Notarization verifies your app is safe - you upload it to Apple’s servers for scanning, and if approved, they “staple” a ticket to your app. The process typically takes a few minutes in Apple’s notarization queue (officially called the “processing queue”). The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh&quot;&gt;notarization script&lt;/a&gt; uses the modern &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution&quot;&gt;&lt;code&gt;notarytool&lt;/code&gt;&lt;/a&gt; approach:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Create ZIP for notarization (not DMG!)&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;ditto&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;c&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;k&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; --&lt;/span&gt;keepParent&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;ZIP_PATH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Submit for notarization&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;xcrun&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; notarytool submit &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;ZIP_PATH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;    --&lt;/span&gt;key&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;API_KEY_PATH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;    --&lt;/span&gt;key-id&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;API_KEY_ID&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;    --&lt;/span&gt;issuer&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;ISSUER_ID&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt;    --&lt;/span&gt;wait&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Staple the ticket&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;xcrun&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; stapler staple &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Verify everything worked&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;spctl&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;t&lt;/span&gt; exec&lt;span class=&quot;syntax-variable syntax-parameter syntax-option syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-parameter syntax-shell&quot;&gt; -&lt;/span&gt;vv&lt;/span&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;xcrun&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; stapler validate &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;APP_BUNDLE&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course this needs &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution&quot;&gt;certificates from Apple&lt;/a&gt;. Luckily Claude is smart enough to guide you through the whole process of downloading those files and adding them to the keychain and your environment.&lt;/p&gt;
&lt;h2&gt;Pretty Changelogs in Sparkle&lt;/h2&gt;
&lt;p&gt;One challenge I hadn’t anticipated was making the update dialogs actually useful. Sparkle can display rich HTML changelogs, but getting from my Markdown changelog to properly formatted HTML required some creativity.&lt;/p&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/changelog-to-html.sh&quot;&gt;changelog-to-html.sh script&lt;/a&gt; extracts version-specific sections from &lt;code&gt;CHANGELOG.md&lt;/code&gt; and converts them to HTML with what I call the “Poor Man’s Markdown Parser”:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Shell&quot;&gt;&lt;span class=&quot;syntax-source syntax-shell syntax-bash&quot;&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell&quot;&gt;#&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt; Extract version section and convert Markdown to HTML&lt;/span&gt;&lt;span class=&quot;syntax-comment syntax-line syntax-number-sign syntax-shell&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;awk&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-double syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;/^## &lt;span class=&quot;syntax-constant syntax-character syntax-escape syntax-shell&quot;&gt;\\&lt;/span&gt;[&lt;span class=&quot;syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-variable syntax-shell&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;syntax-variable syntax-other syntax-readwrite syntax-shell&quot;&gt;version&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-constant syntax-character syntax-escape syntax-shell&quot;&gt;\\&lt;/span&gt;]/{flag=1;next}/^## &lt;span class=&quot;syntax-constant syntax-character syntax-escape syntax-shell&quot;&gt;\\&lt;/span&gt;[/{flag=0}flag&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt; CHANGELOG.md&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sed&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;s/^### \(.*\)/&amp;lt;h3&amp;gt;\1&amp;lt;\/h3&amp;gt;/&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sed&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;s/^- \(.*\)/&amp;lt;li&amp;gt;\1&amp;lt;\/li&amp;gt;/&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell&quot;&gt;\
&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-shell&quot;&gt;&lt;span class=&quot;syntax-variable syntax-function syntax-shell&quot;&gt;sed&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;syntax-meta syntax-function-call syntax-arguments syntax-shell&quot;&gt; &lt;span class=&quot;syntax-string syntax-quoted syntax-single syntax-shell&quot;&gt;&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;s/\*\*\([^*]*\)\*\*/&amp;lt;strong&amp;gt;\1&amp;lt;\/strong&amp;gt;/g&lt;span class=&quot;syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell&quot;&gt;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result? Users see properly formatted changelogs with headers, lists, and styled text instead of raw Markdown. Note: This parser has limitations - it doesn’t handle nested lists, images, or links. Keep your changelogs simple or extend the parser for more complex formatting.&lt;/p&gt;
&lt;h2&gt;The Complete Architecture&lt;/h2&gt;
&lt;p&gt;What emerged is a surprisingly elegant zero-infrastructure solution that leverages GitHub’s existing services:&lt;/p&gt;
&lt;h3&gt;GitHub-Centric Distribution&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Releases&lt;/strong&gt;: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/releases&quot;&gt;GitHub releases&lt;/a&gt; host the actual DMG files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Appcast Hosting&lt;/strong&gt;: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://raw.githubusercontent.com/steipete/VibeMeter/main/appcast.xml&quot;&gt;Raw GitHub URLs&lt;/a&gt; serve the XML feeds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dual Feeds&lt;/strong&gt;: Separate appcasts for &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/appcast.xml&quot;&gt;stable&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/appcast-prerelease.xml&quot;&gt;pre-release&lt;/a&gt; channels&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version Control&lt;/strong&gt;: Appcast files are versioned alongside the code&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dynamic Channel Switching&lt;/h3&gt;
&lt;p&gt;The app includes runtime logic to switch between update channels without reinstallation. Users can choose “stable” for production releases or “pre-release” for beta access, and the app dynamically points to the appropriate appcast URL.&lt;/p&gt;
&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/vibemeter-settings.png&quot; alt=&quot;Vibe Meter settings showing update channel options&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;low&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;h3&gt;Automated Everything&lt;/h3&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh&quot;&gt;release.sh script&lt;/a&gt; orchestrates the entire pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build and sign the app&lt;/li&gt;
&lt;li&gt;Create GitHub release with DMG&lt;/li&gt;
&lt;li&gt;Generate both appcast files with proper signatures&lt;/li&gt;
&lt;li&gt;Commit and push everything&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No separate hosting, no Jekyll setup, no additional infrastructure - just GitHub doing what it does best. The key is that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh&quot;&gt;&lt;code&gt;release.sh&lt;/code&gt;&lt;/a&gt; is the master orchestrator that calls most other scripts in sequence for a complete automated release.&lt;/p&gt;
&lt;p&gt;View the complete script flow architecture&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;&lt;span class=&quot;code-snippet&quot;&gt;🚀 Main Release Flow (release.sh)

release.sh
├── 1. preflight-check.sh ────────────── Validates everything ready
├── 2. generate-xcproj.sh ────────────── Generates Xcode project
├── 3. build.sh ──────────────────────── Builds app with IS_PRERELEASE_BUILD flag
├── 4. sign-and-notarize.sh ──────────── Signs &amp;amp; notarizes app
│   ├── codesign-app.sh ─────────────── Code signs app bundle
│   └── notarize-app.sh ─────────────── Notarizes signed app
├── 5. create-dmg.sh ─────────────────── Creates &amp;amp; signs DMG
├── 6. GitHub CLI (gh) ───────────────── Creates GitHub release
├── 7. generate-appcast.sh ───────────── Updates appcast XML files
└── 8. verify-appcast.sh ─────────────── Validates appcast (optional)

✅ Verification Flow

preflight-check.sh ───────────────────── Pre-release validation
└── verify-prerelease-system.sh ──────── IS_PRERELEASE_BUILD system check

verify-app.sh ────────────────────────── Post-build verification
verify-appcast.sh ────────────────────── Appcast validation

🔐 Manual Signing Flow

sign-and-notarize.sh
├── codesign-app.sh ──────────────────── Code sign app
├── notarize-app.sh ──────────────────── Notarize app
└── create-dmg.sh ────────────────────── Create distribution DMG

🛠️ Utility Scripts (Called by others)

- changelog-to-html.sh ← Called by update-appcast.sh
- version.sh ← Standalone version management
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read the documentation carefully&lt;/strong&gt; - But also know that it might not cover every edge case&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Console.app is your friend&lt;/strong&gt; - Filter by your process name and watch for XPC errors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t be clever&lt;/strong&gt; - Follow Sparkle’s conventions exactly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test updates, not just builds&lt;/strong&gt; - A successful build doesn’t mean updates will work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate everything&lt;/strong&gt; - Manual processes lead to human errors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version control your scripts&lt;/strong&gt; - Build automation is as important as your app code&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Implementing Sparkle in a sandboxed app is like solving a puzzle where the pieces keep changing shape. But once you understand the rules - respect the XPC services, get your entitlements right, and sign everything properly - it works beautifully.&lt;/p&gt;
&lt;p&gt;The irony? The final solution is actually quite simple. It’s getting there that’s the adventure. I don’t know how anyone manages to ship working macOS apps at all, honestly.&lt;/p&gt;
&lt;p&gt;Now go download &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://vibemeter.ai&quot;&gt;Vibe Meter&lt;/a&gt; and read my dev diary on how I built it with Claude Code: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/vibe-meter-monitor-your-ai-costs&quot;&gt;Vibe Meter: Monitor Your AI Costs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sparkle-project.org/documentation/sandboxing/&quot;&gt;Sparkle Sandboxing Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution&quot;&gt;Apple’s Code Signing Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter&quot;&gt;VibeMeter Source Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/tree/main/scripts&quot;&gt;VibeMeter Build Scripts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Special thanks to the Sparkle team and Claude Code - without them, this automation pipeline wouldn’t exist.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; Just &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/tree/main/scripts&quot;&gt;steal my scripts&lt;/a&gt;. It’ll save you days of debugging. ☕️&lt;/p&gt;
&lt;section class=&quot;footnotes&quot; data-footnotes&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;An appcast is an RSS-like XML feed that Sparkle uses to check for updates. It contains information about available versions, download URLs, and release notes. See &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sparkle-project.org/documentation/publishing/&quot;&gt;Sparkle’s documentation on appcasts&lt;/a&gt; for more details. &lt;a href=&quot;#fnref-1&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;1&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn-2&quot;&gt;
&lt;p&gt;The Downloader service is only needed if your app doesn’t have network access. Since Vibe Meter includes the &lt;code&gt;com.apple.security.network.client&lt;/code&gt; entitlement, we can disable SUEnableDownloaderService. This simplifies the setup and avoids the Downloader service’s limitations. See &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://sparkle-project.github.io/documentation/sandboxing/&quot;&gt;Sparkle’s sandboxing documentation&lt;/a&gt; for details. &lt;a href=&quot;#fnref-2&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;2&quot; aria-label=&quot;Back to reference 2&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Vibe Meter: Monitor Your AI Costs</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-04-vibe-meter-monitor-your-ai-costs/</link>
    <guid isPermaLink="false">urn:aggr:item:bd80a092b1ccc51279099b47cf27d44720a0096c</guid>
    <atom:link href="https://steipete.me/posts/2025/vibe-meter-monitor-your-ai-costs/" rel="via" type="text/html"/>
    <pubDate>Wed, 4 Jun 2025 11:00:00 +0000</pubDate>
    <description>How I built Vibe Meter, a macOS menu bar app to track AI spending in real-time - from workshop demo to shipped product in three days.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/vibe-meter/hero.jpg&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;My Cursor bill hit $900 for the month. I needed a simple way to track AI spending without constantly checking dashboards. That frustration sparked &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.vibemeter.ai/&quot;&gt;Vibe Meter&lt;/a&gt; - my first shipped Mac app built in three intense days.&lt;/p&gt;
&lt;p&gt;What started as a workshop demo became a sophisticated menu bar utility using Swift 6, SwiftUI, and extensive AI assistance.&lt;/p&gt;
&lt;h2&gt;What the AIs Say&lt;/h2&gt;
&lt;p&gt;I had several AI models analyze the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter&quot;&gt;VibeMeter codebase&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;O3&lt;/strong&gt;: “Name vs. purpose mismatch – it sounds like a mood tracker, but monitors AI-API spending.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Opus&lt;/strong&gt;: “86% test coverage is extraordinary. Code quality rivals what I see in major tech companies. This isn’t just a ‘side project’ - it’s a case study in how to build macOS software the right way.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gemini&lt;/strong&gt;: “Intentionally over-engineered for future growth.”&lt;/p&gt;
&lt;h2&gt;From Workshop Demo to Production App&lt;/h2&gt;
&lt;p&gt;I ran &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/the-future-of-vibe-coding/&quot;&gt;a live workshop&lt;/a&gt; demonstrating AI-assisted development. Since Cursor lacks a public API, we reverse-engineered their web interface, extracted cookies, and called internal endpoints. Built both Electron and macOS versions in parallel.&lt;/p&gt;
&lt;p&gt;The feedback surprised me - people actually found it useful. When &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/GergelyOrosz&quot;&gt;Gergely Orosz&lt;/a&gt; mentioned linking to it in his newsletter, I spent two more days polishing the rough demo into something shippable.&lt;/p&gt;
&lt;p&gt;Cursor has no public API for cost control, and it was somewhat messy because I have an enterprise account while many people have individual accounts, and the API endpoints are quite different. Getting both right was tricky.&lt;/p&gt;
&lt;p&gt;Special thanks to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/Cocoanetics&quot;&gt;Oliver Drobnik&lt;/a&gt; for helping me fix the individual account parsing. The second 90% of finishing an app always takes longer than the first 90%.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Looking for testers:&lt;/strong&gt; Need someone with multiple Cursor teams to help test. Current implementation assumes one team only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Key Technical Challenges&lt;/h2&gt;
&lt;h3&gt;SwiftUI vs AppKit: The Eternal Dance&lt;/h3&gt;
&lt;p&gt;I used SwiftUI wherever possible, but macOS menu bar apps expose the framework’s limitations. Need a popover without the arrow? Drop down to AppKit. Want to show settings from a menu item? More AppKit gymnastics.&lt;/p&gt;
&lt;p&gt;I needed a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/CustomMenuWindow.swift&quot;&gt;custom NSPanel&lt;/a&gt; for precise menu bar control. Memory management got tricky in release builds - weak references to windows deallocated before appearing.&lt;/p&gt;
&lt;p&gt;Even showing settings required &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/NSApplication%2BopenSettings.swift&quot;&gt;a reflection hack&lt;/a&gt; accessing internal NSMenuItem properties. Apple deprecated the old methods, but the “correct” SwiftUI approach only works from SwiftUI views.&lt;/p&gt;
&lt;h3&gt;The Animation Breakthrough&lt;/h3&gt;
&lt;p&gt;I’m proud of the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/GaugeIcon.swift&quot;&gt;animated menu bar icon&lt;/a&gt;. Almost used Lottie, but SwiftUI was enough. ChatGPT converted a static icon into animatable code.&lt;/p&gt;
&lt;p&gt;The gauge moves with spending levels, uses conic gradients for loading shimmer. ImageRenderer converts SwiftUI to NSImage for the status item.&lt;/p&gt;
&lt;h3&gt;Swift 6 Concurrency Deep Dive&lt;/h3&gt;
&lt;p&gt;This was my first production app using Swift 6’s strict concurrency model. The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/&quot;&gt;architecture heavily relies on actors&lt;/a&gt; for thread safety:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Providers/Cursor/CursorAPIClient.swift&quot;&gt;CursorAPIClient&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift&quot;&gt;ExchangeRateManager&lt;/a&gt; as actors&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@MainActor&lt;/code&gt; isolation for UI components&lt;/li&gt;
&lt;li&gt;Structured concurrency with TaskGroup in &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift&quot;&gt;MultiProviderDataOrchestrator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;async/await&lt;/code&gt; throughout, ditching Combine entirely&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The learning curve was steep - debugging &lt;code&gt;UNUserNotificationCenter&lt;/code&gt; delegate callbacks firing on background threads while trying to update &lt;code&gt;@MainActor&lt;/code&gt; state was particularly painful.&lt;/p&gt;
&lt;p&gt;One bug drove me crazy: perfect in debug, window wouldn’t show in release. SwiftUI view held weakly by AppKit disappeared before display. No crash, just nothing.&lt;/p&gt;
&lt;p&gt;Claude figured it out when I explained the symptoms. Reminder: always test release builds.&lt;/p&gt;
&lt;h3&gt;Apple’s Distribution Gauntlet&lt;/h3&gt;
&lt;p&gt;The hardest part wasn’t coding - it was signing, notarization, and distribution. Apple’s process is error-prone: you can notarize an app but miss deep notarization of embedded frameworks (like Sparkle), leaving you with a partially notarized app that won’t run.&lt;/p&gt;
&lt;p&gt;Rather than wrestle with Fastlane, I built &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/tree/main/scripts&quot;&gt;custom shell scripts&lt;/a&gt; that are much more debuggable. Now Claude can create releases by simply running “make a new release 1.1 beta 1” - the automation is beautiful once set up.&lt;/p&gt;
&lt;p&gt;The scripts handle everything: incrementing build numbers, creating DMGs, uploading to GitHub releases, generating Sparkle appcast files. The most complex part was handling Sparkle’s EdDSA signatures correctly - the error messages when you get it wrong are cryptic at best. Pro tip: always test your update feed locally before pushing.&lt;/p&gt;
&lt;h2&gt;Interesting Technical Details&lt;/h2&gt;
&lt;h3&gt;Multi-Provider Architecture&lt;/h3&gt;
&lt;p&gt;Though &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter&quot;&gt;Vibe Meter&lt;/a&gt; only supports Cursor currently, the architecture anticipates multiple providers. The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/MultiProviderDataOrchestrator.swift&quot;&gt;MultiProviderDataOrchestrator&lt;/a&gt; and protocol-based design make adding OpenAI or Anthropic straightforward when the time comes.&lt;/p&gt;
&lt;p&gt;I really had to hold myself back from implementing OpenAI and Anthropic support right away. When I realized this would be useful for tracking all AI costs - not just Cursor - the temptation was strong. But I knew if I started adding providers, I’d never ship. So I built the architecture to support it but forced myself to ship with just Cursor. This is definitely coming in a future update.&lt;/p&gt;
&lt;p&gt;The multi-provider design might seem like overkill, but it’s already paying dividends. When debugging, I can easily add a mock provider that returns test data. The orchestrator handles provider failures gracefully - if Cursor’s API is down, the app doesn’t crash, it just shows cached data with a subtle indicator.&lt;/p&gt;
&lt;h3&gt;Polish in the Details&lt;/h3&gt;
&lt;p&gt;Small touches matter. The app includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift&quot;&gt;Currency conversion&lt;/a&gt; with real-time exchange rates&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Presentation/Components/UserAvatarView.swift&quot;&gt;Gravatar integration&lt;/a&gt; for user avatars (SHA-256 hashing, retina support)&lt;/li&gt;
&lt;li&gt;Comprehensive &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/NetworkConnectivityMonitor.swift&quot;&gt;network connectivity monitoring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift&quot;&gt;ApplicationMover&lt;/a&gt; for automatic /Applications folder relocation&lt;/li&gt;
&lt;li&gt;Spending limit notifications with &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SpendingLimitsManager.swift&quot;&gt;configurable thresholds&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Agent-Friendly Development&lt;/h3&gt;
&lt;p&gt;I constrained every file to under 300 lines - agents make fewer mistakes with smaller files. This forced better organization and created a more maintainable codebase for both humans and AI. Using &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://tuist.io&quot;&gt;Tuist&lt;/a&gt; for project generation also helped since agents struggle with Xcode’s complex &lt;code&gt;.pbxproj&lt;/code&gt; files.&lt;/p&gt;
&lt;h2&gt;By the Numbers&lt;/h2&gt;
&lt;p&gt;What emerged surprised even me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;174 Swift files&lt;/strong&gt; totaling &lt;strong&gt;28,599 lines&lt;/strong&gt; of code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;86% test coverage&lt;/strong&gt; with 628 individual test methods&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;18 shell scripts&lt;/strong&gt; (2,857 lines) for build automation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;437 occurrences&lt;/strong&gt; of Swift 6 concurrency keywords&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;15 protocols&lt;/strong&gt; for loose coupling and testability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The largest files respect the 300-line constraint: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Utilities/ApplicationMover.swift&quot;&gt;ApplicationMover.swift&lt;/a&gt; (390 lines), &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/SparkleUpdaterManager.swift&quot;&gt;SparkleUpdaterManager.swift&lt;/a&gt; (361 lines), and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Theme/Color%2BTheme.swift&quot;&gt;Color+Theme.swift&lt;/a&gt; (358 lines). The build infrastructure alone required comprehensive scripts for &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/sign-and-notarize.sh&quot;&gt;code signing&lt;/a&gt; (349 lines), &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/setup-sparkle-release.sh&quot;&gt;release automation&lt;/a&gt; (314 lines), and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh&quot;&gt;notarization&lt;/a&gt; (312 lines).&lt;/p&gt;
&lt;h2&gt;The Testing Philosophy&lt;/h2&gt;
&lt;p&gt;I wanted to see how well AI generates tests. For a hobby project like this, I probably wouldn’t have written any. But they caught real bugs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Currency conversion edge cases (what happens when the exchange rate API is down?)&lt;/li&gt;
&lt;li&gt;Cookie expiration handling (Cursor cookies last 30 days, not forever)&lt;/li&gt;
&lt;li&gt;Timezone bugs (monthly spending resets at UTC midnight, not local time)&lt;/li&gt;
&lt;li&gt;The classic “what if the user has no internet?” scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeterTests/Core/Utilities/UserDefaultsBackedAdvancedTests.swift&quot;&gt;UserDefaultsBackedAdvancedTests&lt;/a&gt; alone has 374 lines testing every possible edge case of property wrappers. Did we need all of them? Probably not. Did they catch bugs? Absolutely.&lt;/p&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Context windows matter&lt;/strong&gt;. I ran six Claude instances concurrently - one for linting, one for tests, one for UI, one for features, one for bugs. First time trying this strategy. Worked beautifully. The more an agent knows about previous related tasks, the better it performs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Small files matter&lt;/strong&gt;. The 300-line constraint prevented agent editing mistakes and forced better architecture. What seemed like a limitation became a forcing function for good design.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SwiftUI has come far&lt;/strong&gt;. Despite the AppKit gymnastics required for menu bar specifics, SwiftUI handled 90% of the UI beautifully. The animation capabilities particularly impressed me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Modern Swift is powerful&lt;/strong&gt;. Swift 6’s actor model and structured concurrency made complex state management surprisingly manageable, even for a concurrency novice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI agents love writing tests&lt;/strong&gt;. Embrace it. The comprehensive test suite they generated caught bugs I never would have thought to test for.&lt;/p&gt;
&lt;h2&gt;Performance Notes&lt;/h2&gt;
&lt;p&gt;A menu bar app needs to be lightweight. Vibe Meter uses about 45MB of memory and essentially zero CPU when idle. The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/BackgroundDataProcessor.swift&quot;&gt;BackgroundDataProcessor&lt;/a&gt; refreshes data every 5 minutes by default, but backs off exponentially if errors occur.&lt;/p&gt;
&lt;p&gt;One optimization I’m proud of: the app only loads exchange rates when you actually use a non-USD currency. The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter/blob/main/VibeMeter/Core/Services/ExchangeRateManager.swift&quot;&gt;ExchangeRateManager&lt;/a&gt; caches rates for 24 hours and falls back gracefully if the API is down. Small touches, but they matter for an app that runs 24/7.&lt;/p&gt;
&lt;p&gt;If you’re tracking AI costs, try &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.vibemeter.ai/&quot;&gt;Vibe Meter&lt;/a&gt;. Source on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter&quot;&gt;GitHub&lt;/a&gt;. Watch the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/the-future-of-vibe-coding/&quot;&gt;workshop recording&lt;/a&gt; to see where it started.&lt;/p&gt;
&lt;p&gt;Three days. One person. Six AI assistants. The future is weird.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Claude Code is My Computer</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-03-claude-code-is-my-computer/</link>
    <guid isPermaLink="false">urn:aggr:item:dcc82e82be373b8cfc2b98d38036dd1373f46210</guid>
    <atom:link href="https://steipete.me/posts/2025/claude-code-is-my-computer/" rel="via" type="text/html"/>
    <pubDate>Tue, 3 Jun 2025 08:00:00 +0000</pubDate>
    <description>I run Claude Code with --dangerously-skip-permissions flag, giving it full system access. Let me show you a new way of approaching computers.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/claude-code-is-my-computer/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I run Claude Code in no-prompt mode; it saves me an hour a day and hasn’t broken my Mac in two months. The $200/month &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/stop-overthinking-ai-subscriptions/&quot;&gt;Max plan&lt;/a&gt; pays for itself.&lt;/p&gt;
&lt;p&gt;For the past two months, I’ve been living dangerously. I launch &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://claude.ai/code&quot;&gt;Claude Code&lt;/a&gt; (&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/news/claude-3-7-sonnet&quot;&gt;released in late February&lt;/a&gt;) with &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;, the flag that bypasses all permission prompts. According to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Anthropic’s docs&lt;/a&gt;, this is meant “only for Docker containers with no internet”, yet it runs perfectly on regular macOS.&lt;/p&gt;
&lt;p&gt;Yes, a rogue prompt could theoretically nuke my system. That’s why I keep hourly &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.arqbackup.com/&quot;&gt;Arq&lt;/a&gt; snapshots (plus a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.shirt-pocket.com/SuperDuper/SuperDuperDescription.html&quot;&gt;SuperDuper!&lt;/a&gt; clone), but after two months I’ve had zero incidents.&lt;/p&gt;
&lt;h2&gt;From ‘AI assistant’ to everything terminal&lt;/h2&gt;
&lt;p&gt;When I first installed Claude Code, I thought I was getting a smarter command line for coding tasks. What I actually got was a universal computer interface that happens to run in text. The mental shift took a few weeks, but once it clicked, I realized Claude can literally do anything I ask on my computer.&lt;/p&gt;
&lt;p&gt;The breakthrough moment came when I was migrating to a new Mac. Instead of doing the usual restore dance, I pointed Claude at my backup disk and said: “Restore this Mac from my backup disk—start with dotfiles, then system preferences, CLI tools, and restore Homebrew formulae and global npm packages.” Claude drafts a migration plan, executes it step by step, and has my new machine ready in under an hour.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-1&quot; id=&quot;fnref-1&quot; data-footnote-ref&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;What I actually use it for&lt;/h2&gt;
&lt;p&gt;My daily Claude Code usage falls into several main outcomes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ship Content&lt;/strong&gt;: “Convert ~40 posts from Jekyll to MDX format here. Make sure to copy over the images and preserve the redirects.” Twenty minutes later, Claude had processed every single post, set up proper redirects, validated all image paths, and pushed a merge-ready branch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extract Features&lt;/strong&gt;: “Extract this feature into a Swift project” (that’s how I released &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/introducing-demark-html-to-markdown-in-swift/&quot;&gt;Demark&lt;/a&gt;) where Claude creates the package structure, writes tests, documentation, and handles the entire open-source release process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automate Content&lt;/strong&gt;: Like this very post. I use &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://wisprflow.ai/&quot;&gt;Wispr Flow&lt;/a&gt; to talk with Claude, explain the topic and tell it to read my past blog posts to write in my style. Instead of wrestling with Markdown formatting, Claude creates the document, helps formulate thoughts, and tests that everything displays correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generate Test Data&lt;/strong&gt;: “&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://x.com/steipete/status/1923897903698887036&quot;&gt;Create seed data for a project&lt;/a&gt;” turns into Claude analyzing my codebase, understanding the data models, and generating realistic test data with proper relationships.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ship Code&lt;/strong&gt;: I haven’t typed &lt;code&gt;git commit -m&lt;/code&gt; in weeks. Instead, I say “commit everything in logical chunks” and Claude handles the entire flow—staging changes, writing meaningful commit messages, pushing, opening PRs, watching CI, and fixing any CI failures. When builds break, it analyzes the errors and patches them automatically. It’s also extremely good at resolving merge conflicts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clean the OS&lt;/strong&gt;: “Hide recent apps in the Dock” becomes a single natural language command instead of Googling for the right &lt;code&gt;defaults write&lt;/code&gt; incantation. Claude knows macOS internals and happily calls &lt;code&gt;killall Dock&lt;/code&gt; to restart the Dock after modifying the plist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spin Up New Machines&lt;/strong&gt;: Recently when setting up &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.codelooper.app/&quot;&gt;CodeLooper’s&lt;/a&gt; code signing and notarization, Claude handled installing Homebrew packages, creating private keys, adding them to the keychain, creating backups, building the project, uploading to GitHub, running tests, and monitoring the process. The only manual part was clicking through the update UI, but with my &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/macos-automator-mcp&quot;&gt;macOS Automator MCP Server&lt;/a&gt;, I could probably teach it that too.&lt;/p&gt;
&lt;p&gt;I use an alias in my shell config&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;#fn-2&quot; id=&quot;fnref-2&quot; data-footnote-ref&gt;2&lt;/a&gt;&lt;/sup&gt; so just typing &lt;code&gt;cc&lt;/code&gt; runs Claude with the permission flag.&lt;/p&gt;
&lt;h2&gt;Why this works (and when it doesn’t)&lt;/h2&gt;
&lt;p&gt;Claude Code shines because it was built command-line-first, not bolted onto an IDE as an afterthought. The agent has full access to my filesystem (if you are bold enough…), can execute commands, read output, and iterate based on results.&lt;/p&gt;
&lt;p&gt;Anthropic’s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;best practices guide&lt;/a&gt; recommends keeping a &lt;code&gt;CLAUDE.md&lt;/code&gt; file at your repo root with project-specific context. I’ve adopted this pattern and noticed Claude asks fewer clarifying questions and writes more accurate code. You can check out &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/agent-rules&quot;&gt;my Claude Code rules&lt;/a&gt; for examples of how I structure these files. Little optimizations like this compound quickly.&lt;/p&gt;
&lt;p&gt;The main limitation is response time. Claude’s thinking process takes a few seconds, and for rapid-fire debugging sessions, I sometimes reach for traditional tools. However, you can prefix commands with &lt;code&gt;!&lt;/code&gt; to run them directly without waiting for token evaluation—Claude will execute your command either way, but this is faster when you know exactly what you’re calling. For exploratory work where I’m not sure what I need, Claude’s reasoning ability more than compensates for the brief pause.&lt;/p&gt;
&lt;h2&gt;Why Warp lacks&lt;/h2&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.warp.dev/&quot;&gt;Warp’s&lt;/a&gt; mission is to “reinvent the command line with AI”. They’ve built beautiful GPU-accelerated panels and smart autocomplete.&lt;/p&gt;
&lt;p&gt;The fundamental difference comes down to trust and execution flow. Claude operates purely through text and is remarkably intelligent about understanding context and intent. With this setup, I can pre-authorize Claude to execute commands without constant confirmation prompts. Warp, while excellent, requires individual approval for each command—there’s no equivalent to Claude’s “dangerous mode” where you can grant blanket execution trust. This means Claude maintains conversational flow while Warp still interrupts with permission requests.&lt;/p&gt;
&lt;p&gt;I signed up for Warp because I like their mission and I hope they eventually go where Claude is. But it seems they have a fundamentally different idea about safety. Also, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://ghostty.org/&quot;&gt;Ghostty&lt;/a&gt; is just the better command line, native, not Electron-based and faster.&lt;/p&gt;
&lt;h2&gt;Where this is heading&lt;/h2&gt;
&lt;p&gt;We’re in the very early days of AI-native development tools. Claude Code represents a paradigm shift: from tools that help you run commands to tools that understand intent and take action. I’m not just typing commands faster—I’m operating at a fundamentally higher level of abstraction. Instead of thinking “I need to write a bash script to process these files, chmod it, test it, debug it,” I think “organize these files by date and compress anything older than 30 days.”&lt;/p&gt;
&lt;p&gt;This isn’t about AI replacing developers—it’s about developers becoming orchestrators of incredibly powerful systems. The skill ceiling rises: syntax fades, system thinking shines.&lt;/p&gt;
&lt;h2&gt;Should you try this?&lt;/h2&gt;
&lt;p&gt;If you’re comfortable with calculated risks and have solid backups, absolutely. The learning curve is essentially zero—you just start talking to your computer like it’s a competent colleague. Within days, you’ll wonder how you ever worked without it.&lt;/p&gt;
&lt;p&gt;Your computer isn’t just a computer anymore. It’s Claude. And Claude is absurdly capable.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Got a crazier Claude workflow? Ping me &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twitter.com/steipete&quot;&gt;@steipete&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Essential Reading&lt;/strong&gt;: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://spiess.dev/blog/how-i-use-claude-code&quot;&gt;How I Use Claude Code&lt;/a&gt; is a must-read by my friend and ex-employee Philipp for everyone using Claude Code.&lt;/p&gt;
&lt;section class=&quot;footnotes&quot; data-footnotes&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;Note that full backup migrations can sometimes cause &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://discussions.apple.com/thread/255759421&quot;&gt;various system issues&lt;/a&gt; with newer macOS versions. &lt;a href=&quot;#fnref-1&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;1&quot; aria-label=&quot;Back to reference 1&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn-2&quot;&gt;
&lt;p&gt;&lt;code&gt;alias cc=&amp;quot;claude --dangerously-skip-permissions&amp;quot;&lt;/code&gt; &lt;a href=&quot;#fnref-2&quot; class=&quot;footnote-backref&quot; data-footnote-backref data-footnote-backref-idx=&quot;2&quot; aria-label=&quot;Back to reference 2&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Stop Over-thinking AI Subscriptions</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-02-stop-over-thinking-ai-subscriptions/</link>
    <guid isPermaLink="false">urn:aggr:item:36f7766509ccd8e62fca70a2f688b4f2af9bdb28</guid>
    <atom:link href="https://steipete.me/posts/2025/stop-overthinking-ai-subscriptions/" rel="via" type="text/html"/>
    <pubDate>Mon, 2 Jun 2025 09:00:00 +0000</pubDate>
    <description>After spending heavily on AI tools for two months, here&#39;s why the math actually works out—and which subscriptions are worth every penny.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/stop-overthinking-ai-subscriptions/hero.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;I’ve been working heavily with AI for the past few months, and many times when I post something on Twitter, the replies are predictable: “Oh my god, how much did you pay for that?”&lt;/p&gt;
&lt;p&gt;The thing is, people don’t understand that you don’t actually have to pay &lt;em&gt;that&lt;/em&gt; much to get incredible AI productivity. After using the best AI subscription deals 2025 has to offer, here’s the real math (all prices in USD). (And yes, I built &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/vibe-meter-monitor-your-ai-costs/&quot;&gt;Vibe Meter&lt;/a&gt; to track exactly how much I’m spending.)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Monthly Fee&lt;/th&gt;
&lt;th&gt;Included Usage&lt;/th&gt;
&lt;th&gt;Marginal Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;~45 messages/5h&lt;/td&gt;
&lt;td&gt;Rate limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Max 5×&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;~225 messages/5h&lt;/td&gt;
&lt;td&gt;Rate limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Max 20×&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;~900 messages/5h&lt;/td&gt;
&lt;td&gt;Rate limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;500 fast requests&lt;/td&gt;
&lt;td&gt;$0.04/fast request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI o3&lt;/td&gt;
&lt;td&gt;Pay-per-use&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;$10-40/1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;The Current Best Deal: Claude Max at $200/month&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://claude.ai/settings/billing&quot;&gt;Claude Max&lt;/a&gt; gives you almost unlimited use of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://claude.ai/code&quot;&gt;Claude Code&lt;/a&gt;&lt;/strong&gt;, which is hands-down the best terminal-based AI tool available (next to Cursor). For $200/month, you get 20× the usage of Claude Pro.&lt;/p&gt;
&lt;p&gt;Claude Max works on a rolling 5-hour block system with roughly &lt;strong&gt;900 messages per 5-hour window&lt;/strong&gt;. The moment you send your first prompt, the timer starts. When 5 hours are up, everything refreshes with a new block and full quota. The Claude Max subscription uses Opus as the default model and switches to Sonnet when you reach 50% of the 5-hour limit (this is configurable). If you really burn through your whole allowance for that window, you can switch to API-based billing.&lt;/p&gt;
&lt;p&gt;If you’re tight on money and you really need this extremely high use case, look at the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://support.anthropic.com/en/articles/11174108-about-the-development-partner-program&quot;&gt;development partner program&lt;/a&gt; where they offer 30% off standard API pricing (this discount applies only to API usage, not the Max sub).&lt;/p&gt;
&lt;h2&gt;But What About Future Price Increases?&lt;/h2&gt;
&lt;p&gt;I get messages all the time from people worried that token prices will go up. Here’s the reality check:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token prices have dropped 1000× in the last two years.&lt;/strong&gt; We’ve gone from GPT-3’s astronomical pricing to incredibly capable open models. The competitive landscape has completely transformed—companies are building their own models now. Windsurf has SWE-1, Vercel launched v0-1.5lg, and every major player is racing to offer the best value.&lt;/p&gt;
&lt;p&gt;What indicators suggest prices will increase? None. If anything, competition is driving costs down to the fundamental floor: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://blog.samaltman.com/the-gentle-singularity&quot;&gt;energy costs&lt;/a&gt;. Models are becoming commodities. The real differentiator is the tooling around them, not the raw compute.&lt;/p&gt;
&lt;p&gt;So stop worrying about hypothetical price hikes. The trend line points one direction: down.&lt;/p&gt;
&lt;h2&gt;Cursor: The Math That Makes Sense&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://cursor.com/&quot;&gt;Cursor&lt;/a&gt; Pro costs $20/month&lt;/strong&gt; and gives you 500 “fast” requests plus unlimited “slow” ones. Beyond that, it’s &lt;strong&gt;$0.04 per fast request&lt;/strong&gt; (per Cursor docs).&lt;/p&gt;
&lt;p&gt;Cursor offers two pricing modes: &lt;strong&gt;Normal mode&lt;/strong&gt; charges a fixed number of requests per message regardless of context size, while &lt;strong&gt;Max mode&lt;/strong&gt; uses token-based pricing (API cost + 20% margin) for complex reasoning tasks. Most everyday coding works perfectly fine in Normal mode where each message to Gemini 2.5 Pro (my favorite model) costs exactly 1 request.&lt;/p&gt;
&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/the-future-of-vibe-coding/&quot;&gt;That workshop I did&lt;/a&gt;? Probably 200+ AI requests across 3 hours. Let’s do the math: 200 requests × $0.04 = &lt;strong&gt;$8&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For an afternoon of intensive coding with AI assistance, you’re looking at pocket change. Even if you burn through 1,000 requests in a month, that’s $40 on top of your $20 subscription. When you run out of fast requests, Cursor automatically switches to free requests (processed at lower priority) so you never truly hit a wall.&lt;/p&gt;
&lt;h2&gt;What Actually Gets Expensive: OpenAI’s o3&lt;/h2&gt;
&lt;p&gt;Here’s where I’ll be honest: &lt;strong&gt;about half my monthly AI bill goes to o3&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;OpenAI’s o3 model costs &lt;strong&gt;$10 per 1M input tokens and $40 per 1M output tokens&lt;/strong&gt;. Cached input drops to $2.50/1M. When you’re debugging complex code and you want the absolute best reasoning, o3 delivers, but it adds up fast.&lt;/p&gt;
&lt;p&gt;o3 is my dark knight—silent, pricey, but rescues impossible bugs.&lt;/p&gt;
&lt;p&gt;I spent around $400 on o3 last month because I was banging my head against a wall with some really tricky code. When I hit &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/AXorcist&quot;&gt;truly difficult problems&lt;/a&gt;, throwing o3 at them for a few hours beats getting stuck in debugging rabbit holes for days.&lt;/p&gt;
&lt;h2&gt;Smart Alternative: Repo Prompt for o3 Access&lt;/h2&gt;
&lt;p&gt;If you’re tight on money but still want o3’s reasoning power, I highly recommend &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://repoprompt.com/&quot;&gt;Repo Prompt&lt;/a&gt; (macOS-only). It lets you use o3 through your ChatGPT subscription instead of paying API rates.&lt;/p&gt;
&lt;p&gt;While it’s more manual than integrated tools, it’s incredibly effective at taking code changes and applying them to your project. The workflow is straightforward: export your repo context, get o3’s analysis and suggestions, then apply the changes back to your codebase.&lt;/p&gt;
&lt;p&gt;It’s an amazing tool that bridges the gap between premium AI access and budget constraints. Definitely worth experimenting with.&lt;/p&gt;
&lt;h2&gt;The Contractor Math&lt;/h2&gt;
&lt;p&gt;Let’s be conservative and say &lt;strong&gt;$800/day&lt;/strong&gt; (though I’d assume many of you charge more). The AI subscription math is a no-brainer.&lt;/p&gt;
&lt;p&gt;One afternoon saved per month = $200 in billable time. Claude Max pays for itself in 5 saved hours. Cursor pays for itself in 45 minutes.&lt;/p&gt;
&lt;p&gt;The productivity multiplier is so absurd that arguing about the cost is like complaining about the price of coffee while billing $200/hour.&lt;/p&gt;
&lt;p&gt;Now you know—don’t be a Mario:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hi, my name is Mario, and I spent about $1000 in Anthropic pay-as-you-go before switching to Max and finding out that I stay well within the rate limit...&lt;/p&gt;
&lt;p&gt;Don&#39;t be a Mario.&lt;/p&gt;
&lt;p&gt;— Mario Zechner (@badlogicgames) &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twitter.com/badlogicgames/status/1929665561715204520?ref_src=twsrc%5Etfw&quot;&gt;November 30, 2024&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;Time is the only non-refillable resource. Claude Max at $200 is currently the cheapest way I know to mint extra hours—and those hours compound faster than any SaaS line item ever will.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What’s your monthly AI spend?&lt;/strong&gt; Ping me on Twitter &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twitter.com/steipete&quot;&gt;@steipete&lt;/a&gt; with your breakdown and what you’re buying with the saved time.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Introducing Demark: HTML in. MD out. Blink-fast.</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-01-introducing-demark-html-in-md-out-blink-fast/</link>
    <guid isPermaLink="false">urn:aggr:item:4df43bafc8c067509c92fb7178b6ac21414b5f73</guid>
    <atom:link href="https://steipete.me/posts/2025/introducing-demark-html-to-markdown-in-swift/" rel="via" type="text/html"/>
    <pubDate>Sun, 1 Jun 2025 18:45:00 +0000</pubDate>
    <description>How I vibe coded my first Swift package using existing JavaScript libraries and AI assistance to solve HTML to Markdown conversion in Swift.</description>
    <content:encoded>&lt;p&gt;&lt;picture class=&quot;article-picture article-picture-fallback&quot; style=&quot;--image-ratio:16 / 9&quot;&gt;&lt;img src=&quot;https://steipete.me/assets/img/2025/demark/banner.png&quot; alt=&quot;&quot; loading=&quot;eager&quot; decoding=&quot;async&quot; fetchpriority=&quot;high&quot; referrerpolicy=&quot;no-referrer&quot; class=&quot;progressive-image&quot;&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;p&gt;I recently found myself needing a component that converts messy, potentially malformed HTML to clean Markdown in Swift. After searching around, I couldn’t find anything that really fit my needs, so I decided to write my own. The result is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Demark&quot;&gt;Demark&lt;/a&gt; – my first Swift package that’s now available on the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://swiftpackageindex.com/steipete/Demark&quot;&gt;Swift Package Index&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;HTML to Markdown conversion sounds straightforward, but it’s surprisingly complex when you need to handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Malformed or messy HTML&lt;/li&gt;
&lt;li&gt;JavaScript-rendered content&lt;/li&gt;
&lt;li&gt;Complex nested structures&lt;/li&gt;
&lt;li&gt;Various HTML edge cases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I needed something reliable that could handle real-world HTML, not just clean, well-formed markup.&lt;/p&gt;
&lt;h2&gt;The Solution: Standing on the Shoulders of Giants&lt;/h2&gt;
&lt;p&gt;Instead of writing a full-blown HTML parser from scratch, I took a different approach. Why reinvent the wheel when there are already excellent JavaScript libraries that solve this problem really well?&lt;/p&gt;
&lt;p&gt;Demark leverages two proven JavaScript libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/mixmark-io/turndown&quot;&gt;Turndown.js&lt;/a&gt;&lt;/strong&gt; for the most accurate conversions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/stonehank/html-to-md&quot;&gt;html-to-md&lt;/a&gt;&lt;/strong&gt; for faster, lightweight processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The magic happens by running these libraries inside a WKWebView, giving me access to a full browser environment with proper DOM parsing capabilities.&lt;/p&gt;
&lt;h2&gt;Two Conversion Engines&lt;/h2&gt;
&lt;p&gt;Demark offers flexibility with two different engines:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/mixmark-io/turndown&quot;&gt;Turndown.js&lt;/a&gt; (Default)&lt;/strong&gt; - When you need accuracy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full DOM-based parsing&lt;/li&gt;
&lt;li&gt;Handles complex HTML and JavaScript-rendered content&lt;/li&gt;
&lt;li&gt;Comprehensive configuration options&lt;/li&gt;
&lt;li&gt;~100ms for first conversion (then much faster)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/stonehank/html-to-md&quot;&gt;html-to-md&lt;/a&gt;&lt;/strong&gt; - When you need speed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lightweight JavaScript-based conversion&lt;/li&gt;
&lt;li&gt;Much faster performance (~5-10ms per conversion)&lt;/li&gt;
&lt;li&gt;Perfect for batch processing simple HTML&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vibe Coding with AI&lt;/h2&gt;
&lt;p&gt;Here’s the thing – I probably would never have released this without AI assistance. But with AI, the whole process became so much easier. What would have taken me days of tedious work – setting up the package structure, writing documentation, handling edge cases, making it Swift Package Index ready – took me just a few hours.&lt;/p&gt;
&lt;p&gt;AI helped me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structure the Swift package properly&lt;/li&gt;
&lt;li&gt;Write comprehensive documentation&lt;/li&gt;
&lt;li&gt;Handle all the WebKit integration details&lt;/li&gt;
&lt;li&gt;Create a clean, modern Swift API with async/await&lt;/li&gt;
&lt;li&gt;Set up proper error handling&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Simple to Use&lt;/h2&gt;
&lt;p&gt;Despite the complexity under the hood, Demark is dead simple to use. The Swift package that turns down HTML and turns up Markdown – it’s a markup markdown!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;import Demark

@MainActor 
func convertHTML() async throws {
    let demark = Demark()
    let html = &amp;quot;&amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;This is &amp;lt;strong&amp;gt;bold&amp;lt;/strong&amp;gt; text.&amp;lt;/p&amp;gt;&amp;quot;
    let markdown = try await demark.convertToMarkdown(html)
    print(markdown)
    // Output: # Hello World
    //
    // This is **bold** text.
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;My First Swift Package&lt;/h2&gt;
&lt;p&gt;Demark works across all Apple platforms: iOS, macOS, watchOS, tvOS, and visionOS.&lt;/p&gt;
&lt;p&gt;I’m quite happy about this milestone. After years of iOS development, publishing my first Swift package feels like a natural next step in my open source journey. The fact that it’s now in the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://swiftpackageindex.com/steipete/Demark&quot;&gt;Swift Package Index&lt;/a&gt; makes it easily discoverable and installable for other developers.&lt;/p&gt;
&lt;p&gt;The experience taught me that sometimes the best solution isn’t writing everything from scratch – it’s about cleverly combining existing, battle-tested solutions in a new way.&lt;/p&gt;
&lt;h2&gt;Try It Out&lt;/h2&gt;
&lt;p&gt;If you need HTML to Markdown conversion in your Swift projects, give &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Demark&quot;&gt;Demark&lt;/a&gt; a try. You can add it to your project via Swift Package Manager:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;code-snippet&quot; data-language=&quot;Swift&quot;&gt;dependencies: [
    .package(url: &amp;quot;https://github.com/steipete/Demark.git&amp;quot;, from: &amp;quot;1.0.0&amp;quot;)
]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The repository includes an example app with a dual-pane interface where you can test conversions in real-time. Perfect for seeing exactly how your HTML will be transformed.&lt;/p&gt;
&lt;p&gt;Here’s to many more Swift packages! 🚀&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>The Future of Vibe Coding: Building with AI, Live and Unfiltered</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-01-the-future-of-vibe-coding-building-with-ai-live-and/</link>
    <guid isPermaLink="false">urn:aggr:item:9f711cffdf02d17d0773f7dd3ab83c39da3c33d4</guid>
    <atom:link href="https://steipete.me/posts/2025/the-future-of-vibe-coding/" rel="via" type="text/html"/>
    <pubDate>Sun, 1 Jun 2025 15:00:00 +0000</pubDate>
    <description>I demonstrate &#39;vibe coding&#39; - a new approach to software development with AI, building two apps from scratch in a 3-hour live workshop.</description>
    <content:encoded>&lt;p&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://player.vimeo.com/video/1088443203?h=37824a9349&amp;amp;badge=0&amp;amp;autopause=0&amp;amp;player_id=0&amp;amp;app_id=58479&quot;&gt;The future of vibe-coding - Workshop Peter Steinberger - 2025_05_28 08_57 CEST&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A friend asked me to show off my current workflow, so I did an impromptu workshop for him and his developers. This is a snapshot of how I approach vibe coding these days. Don’t expect a professional workshop - I did this as a favor and for the fun of it. I let Gemini extract the key points into a blog post, if you don’t want to watch all of the 3-hour video.&lt;/p&gt;
&lt;p&gt;All the code generated during this workshop is available on &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/VibeMeter&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Transcript&lt;/h2&gt;
&lt;p&gt;Alright, let’s talk about how software development is changing, right now. For nearly 15 years, I was deep in the PDF world with PSPDFKit (now they rebranded as Nutrient). After selling the company, I took a much-needed breather. I was properly burned out. But a couple of months ago, that itch, that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://steipete.me/posts/2025/finding-my-spark-again/&quot;&gt;spark&lt;/a&gt;, came back. And what pulled me in? AI.&lt;/p&gt;
&lt;p&gt;It’s a completely different landscape now. Everything I’m about to share is what I’ve crammed into my brain over the last two, very intense, months. I’m still discovering new things daily, but what’s already possible is, frankly, incredibly cool. So, I decided to do a live workshop: go from a raw idea to a (hopefully) working product in a few hours, unscripted, just pure “vibe coding.”&lt;/p&gt;
&lt;h3&gt;The Idea: “Vibe Meter” – Keeping an Eye on AI Costs&lt;/h3&gt;
&lt;p&gt;If you’re like me, you’re probably diving deep into AI tools. I’m using Cursor (the AI-first IDE) and Anthropic’s Claude Code (their command-line AI) pretty heavily. These things aren’t free. My Cursor bill was already nudging $900 for the month, and there’s that $1000 cap before you need to talk enterprise. It got me thinking: wouldn’t it be great to have a little something in the menu bar to see that spend in real-time?&lt;/p&gt;
&lt;p&gt;And thus, “Vibe Meter” was born. I even briefly considered calling it “Aufpasser” (German for a kind of minder or little policeman – seemed fitting!). The plan was ambitious for a short workshop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A macOS menu bar app (because native is nice).&lt;/li&gt;
&lt;li&gt;An Electron app (for that cross-platform reach, because why not?).&lt;/li&gt;
&lt;li&gt;Display current monthly Cursor spend.&lt;/li&gt;
&lt;li&gt;Show more details on click, like who’s logged in.&lt;/li&gt;
&lt;li&gt;Configurable warning limits – say, a “vibe code limit” at $200, and another at the $1000 mark, with actual user notifications.&lt;/li&gt;
&lt;li&gt;A proper settings dialog for these limits.&lt;/li&gt;
&lt;li&gt;Since Cursor doesn’t have a public API for this, we’d need to figure out a login mechanism, likely by capturing cookies like their own web app does.&lt;/li&gt;
&lt;li&gt;Currency conversion (dollars, Euros, maybe even Philippine Pesos for the team!).&lt;/li&gt;
&lt;li&gt;The usuals: launch at startup, quit option, logout.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yeah, a bit much for a live demo, but go big or go home, right?&lt;/p&gt;
&lt;h3&gt;Phase 1: The Blueprint – Forging Specs with Google’s AI Studio &amp;amp; Gemini&lt;/h3&gt;
&lt;p&gt;Even in this new AI-driven world, a solid specification is still your bedrock. The difference? The process of creating it can be far more dynamic and, dare I say, fun. For this, I turned to Google’s AI Studio, powered by their Gemini model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why AI Studio for Specification? It’s About the Output and the Process.&lt;/strong&gt; Gemini is seriously impressive. I find it’s incredibly good at generating lengthy, structured text without the aggressive summarization or “shortcut-taking” I’ve seen from other models when you ask for, say, full technical documentation. The playground for Gemini seems to have almost unlimited processing power for this kind of task – it doesn’t feel like it’s constantly trying to save on tokens when you need detail. Plus, the cost is quite reasonable – something like &lt;strong&gt;four cents per million tokens&lt;/strong&gt; for the input, which is great for feeding it lots of context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My Spec Refinement Loop – AI Critiquing AI:&lt;/strong&gt; This is a pattern I’ve found incredibly powerful:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Voice-First Brain Dump:&lt;/strong&gt; I’m lazy with typing. I use an app called “Flow” (or Whisper Flow – just hit the &lt;code&gt;Fn&lt;/code&gt; key on my Mac, and it transcribes) to dictate my initial thoughts, feature ideas, and user stories directly into a new Gemini chat in AI Studio. It’s fast and captures ideas fluidly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini’s First Draft:&lt;/strong&gt; Gemini takes this verbal stream-of-consciousness and churns out a pretty decent first-pass structured specification document.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “Peer Review” by Another AI:&lt;/strong&gt; This is the fun part. I copy that entire generated spec and paste it into a &lt;em&gt;brand new, separate&lt;/em&gt; Gemini chat. My prompt to this second instance is something like: &lt;em&gt;“You are an AI tasked with implementing this spec in one go. Before you start, what questions do you have? What’s unclear? What’s missing for you to build this successfully?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Addressing the AI’s Questions:&lt;/strong&gt; The second AI &lt;em&gt;always&lt;/em&gt; comes back with brilliant questions. It points out ambiguities, edge cases I hadn’t considered, and missing information. It’s like having an infinitely patient, detail-oriented peer reviewer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refining the Master Spec:&lt;/strong&gt; I take these questions, think through the answers, and then feed those answers and clarifications back into the &lt;em&gt;original&lt;/em&gt; AI Studio chat – the one that holds the “master” context of our evolving spec.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iteration is Key:&lt;/strong&gt; We went back and forth. For Vibe Meter, this helped us realize we needed to be very specific about how we’d get the Cursor &lt;code&gt;teamId&lt;/code&gt; and the exact cookie name (which turned out to be &lt;code&gt;workos_cursor_session&lt;/code&gt;). This led to a bit of live “API archeology” using browser dev tools – inspecting Cursor’s own web app’s network requests during login, when viewing usage, etc. We found the &lt;code&gt;/me&lt;/code&gt; endpoint for user details and the &lt;code&gt;getMonthlyInvoice&lt;/code&gt; call (a POST request, interestingly) which contained the team ID and costs (in cents).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nailing Down Technicals:&lt;/strong&gt; We also layered in technical constraints: Swift 6 for the Mac app (because why not use the latest?), latest macOS only, Swift Test for unit tests, a strict “menu bar only, no Dock icon” policy, and for the Electron version, a clear directive to use TypeScript and React.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This process of letting an AI help refine the input for &lt;em&gt;another&lt;/em&gt; AI (or even itself in a later stage) is a cornerstone of “vibe coding.” The better your spec, the less friction you’ll have when it’s time to generate actual code. Gemini’s massive 1 million token context window is a godsend here; you can really build up a rich, detailed understanding within the chat.&lt;/p&gt;
&lt;h3&gt;Phase 2: Unleashing the Coders – Cursor IDE, Parallel Builds, and My Bag of AI Tricks&lt;/h3&gt;
&lt;p&gt;With a spec that felt reasonably robust (for a live demo!), it was time to fire up Cursor, my AI-first IDE of choice, and actually generate some code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Going Parallel – Mac &amp;amp; Electron Simultaneously:&lt;/strong&gt; I’d already set up a GitHub repo for “Vibe Meter” and, within it, two distinct folders: &lt;code&gt;VibeMeter-Mac&lt;/code&gt; and &lt;code&gt;VibeMeter-Electron&lt;/code&gt;. The plan was to build both in parallel, so I opened two separate Cursor IDE windows, one pointed at each sub-project.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;For the Mac App (SwiftUI):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;In the Cursor window for &lt;code&gt;VibeMeter-Mac&lt;/code&gt;, I started a new chat.&lt;/li&gt;
&lt;li&gt;Pasted our full, refined specification.&lt;/li&gt;
&lt;li&gt;Selected the Gemini model within Cursor (it’s one of several you can choose).&lt;/li&gt;
&lt;li&gt;The prompt was straightforward: &lt;em&gt;“You are an AI tasked with building a Mac app based on this software specification. Generate the project and all necessary files.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For the Electron App (TypeScript/React):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;In the &lt;code&gt;VibeMeter-Electron&lt;/code&gt; Cursor window, I did the same but with a crucial addition to the prompt: &lt;em&gt;“We also want to build a version of this using Electron. Any technical choices in the spec that were Swift-only should be replaced with good cross-platform alternatives. Use TypeScript and React for the implementation.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And then, the magic (and sometimes the madness) begins. You hit “go,” and the AIs start chugging away, planning steps, creating files, and writing code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dealing with the “Continue Monkey” – And My Solution, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://codelooper.app&quot;&gt;CodeLooper&lt;/a&gt;:&lt;/strong&gt; One of the current papercuts in these AI-driven IDEs is the generation loop. They’ll work for a bit, then pause, often requiring a manual click on a “Continue” button. This can be due to internal limits (Cursor sometimes has a 25-turn limit before pausing), connection hiccups, or the AI just needing a moment to plan its next multi-step move.&lt;/p&gt;
&lt;p&gt;This constant need to babysit the “Continue” button is why I’m building a side-project called “&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://codelooper.app&quot;&gt;CodeLooper&lt;/a&gt;.” It’s a Mac menu bar app that uses a combination of screen capture, accessibility APIs, and even JavaScript injection (since Cursor itself is an Electron app) to detect when a generation loop has paused and, if it deems it safe, automatically click “Continue” for me. It’s still a work in progress, but the goal is to achieve longer, truly unattended generation sessions. For this workshop, though, I was the designated “Continue Monkey.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MCPs – My Custom Toolkit for Supercharging Cursor:&lt;/strong&gt; Cursor allows you to define “MCPs” (which I think of as “More Capable Prompts” or “More Capable Tools”). These are essentially custom functions or external tools the AI agent within Cursor can choose to call. I’ve been building out a suite of these to give the AI more leverage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🧠 &lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/claude-code-mcp&quot;&gt;Claude Code&lt;/a&gt;&lt;/strong&gt;: This is a big one. It allows the Gemini agent in Cursor to delegate specific tasks to Anthropic’s Claude Code running in my terminal (often in “YOLO mode,” where I pre-authorize it to bypass permission prompts for shell commands – live dangerously, right?). This is fantastic for playing to each AI’s strengths. If Cursor gets stuck or I want Claude’s deeper reasoning on a complex refactor, I can have Gemini call out. It’s also cost-effective if you’re on Claude’s flat-rate subscription. A buddy for your IDE that your agent can ask if he’s stuck. Can do coding tasks and offer “a pair of fresh eyes” that often un-stucks the loop.&lt;/li&gt;
&lt;li&gt;👻 &lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Peekaboo&quot;&gt;Peekaboo&lt;/a&gt;&lt;/strong&gt;: Enables your IDE to make screenshots and ask questions about images. For example: &lt;em&gt;“Is this settings screen blank? Does the button look enabled?”&lt;/em&gt; This is a step towards visual self-correction.&lt;/li&gt;
&lt;li&gt;🐱 &lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/conduit-mcp&quot;&gt;Conduit&lt;/a&gt;&lt;/strong&gt;: Advanced file manipulation for faster refactoring. Provides more robust and reliable file system operations (creating directories, moving files, bulk reading/writing) than what the AI might try with basic terminal commands.&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Terminator&quot;&gt;Terminator&lt;/a&gt;&lt;/strong&gt;: Manages a Terminal outside of the loop, so processes that might get stuck don’t break the loop. Crucial for web development. If the AI needs to run a dev server (&lt;code&gt;npm run dev&lt;/code&gt;), this MCP runs it in a properly detached, non-blocking external shell. This prevents the main AI generation loop from getting stuck waiting for a server process that never exits on its own.&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/macos-automator-mcp&quot;&gt;Automator&lt;/a&gt;&lt;/strong&gt;: AppleScript for your IDE.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These MCPs are about extending the AI’s reach and making it more autonomous in a practical development workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Choosing Your Fighter – AI Models in Cursor:&lt;/strong&gt; Cursor offers a selection of underlying LLMs, and I switch between them based on the task:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gemini 2.5 Pro (May 6 Update):&lt;/strong&gt; This is my workhorse for most code generation within Cursor. It strikes a great balance of intelligence, speed, and cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;o3:&lt;/strong&gt; This is what I call my “Dark Knight.” It’s incredibly smart, uncannily good at reasoning through complex problems, but it’s &lt;em&gt;significantly&lt;/em&gt; more expensive. I reserve o3 for when I’m truly stumped on a gnarly bug. You feed it the problem, and it often goes silent for a bit, “planning its next moves,” reading more of your codebase, and then, like that brilliant but slightly arrogant senior dev, it’ll often just present the solution with minimal fuss. It almost &lt;em&gt;always&lt;/em&gt; cracks the problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Opus:&lt;/strong&gt; Also very capable. I find it sometimes a bit more prone to “vision quests” – deviating from the original task if it hits a snag, or getting stuck in its own reasoning loops (like repeatedly trying to downgrade a library if it encounters an issue with a newer version). But for certain types of refactoring or generation, it’s excellent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;During the initial generation, the Mac app (SwiftUI) ran into some turbulence. Swift 6 has tightened up its concurrency rules considerably (Sendability, &lt;code&gt;@MainActor&lt;/code&gt; isolation), and there’s simply less Swift 6 example code out in the wild for the models to have trained on. This meant more iteration was needed. The Electron app, on the other hand, initially started life as JavaScript because, in my haste, I forgot to explicitly re-state TypeScript in the main project generation prompt (the spec mentioned it, but the top-level instruction to the AI didn’t). A good lesson: be explicit, even if it feels redundant. I later instructed it to convert the whole thing to TypeScript.&lt;/p&gt;
&lt;h3&gt;Phase 3: The Intricate Dance of Debugging, Refinement, and Iteration&lt;/h3&gt;
&lt;p&gt;This is where “vibe coding” truly comes alive. The AI isn’t a magic wand that spits out perfect, production-ready code on the first try. Far from it. The initial output will have bugs, missing pieces, UI that needs a loving touch (or a complete overhaul), and architectural decisions you might want to revisit. The beauty is how &lt;em&gt;fast&lt;/em&gt; you can iterate with AI as your partner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Navigating the Unscripted Path – Workshop Highlights:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Taming Xcode’s Beast (The &lt;code&gt;.pbxproj&lt;/code&gt; file):&lt;/strong&gt; Xcode project files are notoriously complex. Rather than having the AI in Cursor wrestle with directly manipulating that XML monstrosity, I took a different route for the Swift app. I opened a separate terminal window and used my &lt;code&gt;claude-code-mcp&lt;/code&gt; to have Claude Code set up the project using &lt;strong&gt;Tuist&lt;/strong&gt;. Tuist is a fantastic open-source tool that generates Xcode projects from a much simpler Swift-based manifest file and a conventional folder structure. I even pointed Claude to another project of mine that already uses Tuist, essentially saying, “Use this as a template.” This is a great example of using the right tool (and AI) for the right job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Icons, Because Aesthetics Matter (Eventually):&lt;/strong&gt; While the AI was churning, I popped over to an image generation model (like DALL-E, accessible through ChatGPT or other interfaces) and prompted it for app icon ideas: &lt;em&gt;“Vibe Meter, cost control app, macOS, cool, modern.”&lt;/em&gt; Then, more specifically for the menu bar: &lt;em&gt;“Monochrome, tintable, simple lines, Mac OS menu bar icon for a cost tracking app.”&lt;/em&gt; Got some decent starting points.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live Debugging – The Swift App’s Journey:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Main Actor Crash:&lt;/strong&gt; Sure enough, the app crashed on first launch. The culprit? A &lt;code&gt;UNUserNotificationCenter&lt;/code&gt; delegate callback was firing on a background thread, but trying to update UI or state that was (correctly) marked as &lt;code&gt;@MainActor&lt;/code&gt; isolated. Classic Swift concurrency. I switched the active model in that Cursor chat to o3 (the Dark Knight!), fed it the crash log and a brief explanation, and o3, true to form, pinpointed the issue and applied the fix: judicious use of &lt;code&gt;nonisolated&lt;/code&gt; on the delegate conformance and wrapping the UI-touching callback code in a &lt;code&gt;Task { @MainActor in ... }&lt;/code&gt; block. Problem solved in minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Menu Bar Shenanigans:&lt;/strong&gt; Initially, the menu bar displayed the text “Login Required” when not logged in, instead of our nice (placeholder) icon. A quick clarification to the AI sorted that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Off-By-One API Bug:&lt;/strong&gt; The API call to fetch the cost data was consistently returning an empty dataset or an error. This was a head-scratcher. By adding some logging and then carefully inspecting the JSON payload from Cursor’s &lt;em&gt;actual&lt;/em&gt; web interface (using browser dev tools), we spotted it: our app was correctly getting the current month (e.g., May as &lt;code&gt;5&lt;/code&gt;), but the Cursor API expected months to be &lt;em&gt;zero-indexed&lt;/em&gt; (so May should be &lt;code&gt;4&lt;/code&gt;). A classic off-by-one! I took screenshots of my logging output and the browser’s network inspector, pasted them into the chat, and explained the discrepancy. The AI got it immediately and adjusted the date calculation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI Polish:&lt;/strong&gt; The initial settings screen was… functional. A prompt like, &lt;em&gt;“Make this settings screen look more like a native, clean macOS settings panel, using standard SwiftUI controls,”&lt;/em&gt; led to a much-improved version.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live Debugging – The Electron App’s Adventure:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Test Setup:&lt;/strong&gt; Playwright tests were part of the spec, and the AI set them up, but they needed some initial nudging to run correctly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Styling Woes:&lt;/strong&gt; The Electron settings screen was unstyled for a bit. &lt;em&gt;“It looks like the CSS for the settings dialog isn’t loading correctly. Please investigate and fix.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Login Logic:&lt;/strong&gt; Getting the web view in Electron to correctly capture the session cookie after a successful login and then trigger the necessary API calls required a few rounds of refinement.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My General AI Debugging &amp;amp; Refinement Workflow:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Screenshots are Your Best Friend:&lt;/strong&gt; Seriously, a picture (or a screenshot of a crash log/weird UI) is worth a thousand words to these multimodal AIs. They can often “see” the problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Focused, Iterative Prompts:&lt;/strong&gt; Instead of massive, multi-point requests, I tend to give short, focused instructions: &lt;em&gt;“Fix this crash.”&lt;/em&gt; &lt;em&gt;“Change the button color to blue.”&lt;/em&gt; &lt;em&gt;“Refactor this function to be more readable.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delegate Complex Refactors to Claude Code (CLI):&lt;/strong&gt; For bigger tasks like adding linters (we integrated SwiftLint for Swift and ESLint/Prettier for the Electron app) or that big “JavaScript to TypeScript” conversion for the Electron app, I often switch to a terminal and let Claude Code handle it. It excels at these broader, codebase-wide transformations and tends to run for longer stretches without needing intervention.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embrace AI Self-Correction:&lt;/strong&gt; Often, the AI will generate code &lt;em&gt;and&lt;/em&gt; tests for that code. If the tests fail, and the AI understands the failure in relation to the spec it was given, it can often figure out the bug in its own generated code and fix it. This self-correction loop is powerful.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Throughout this three-hour whirlwind, I was an orchestrator, a prompter, a quality checker, and a debugger-by-proxy. I wrote &lt;strong&gt;zero&lt;/strong&gt; lines of functional code myself. All the Swift, TypeScript, HTML, CSS, JSON configs – everything came from the AI, guided by my prompts and the initial spec.&lt;/p&gt;
&lt;h3&gt;The Outcome: Two (Mostly) Functional Apps and a Whole Lot of Vibe&lt;/h3&gt;
&lt;p&gt;So, after about three hours of intense, unscripted, AI-driven development, where did we land?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vibe Meter (Mac - SwiftUI):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;It launched! The menu bar icon appeared (though we wrestled a bit to get the custom PNG to show consistently; sometimes it defaulted to text).&lt;/li&gt;
&lt;li&gt;Clicking it revealed my current (mocked, then actual) Cursor spending (e.g., “$895.72”) and my company name (“Amatøs Måchinå” – a little inside joke).&lt;/li&gt;
&lt;li&gt;The login flow worked: it presented a web view for Cursor’s login page, successfully captured the session cookie upon successful login, and securely stored it in the macOS keychain.&lt;/li&gt;
&lt;li&gt;It then correctly used that cookie to hit Cursor’s internal &lt;code&gt;/me&lt;/code&gt; endpoint (to get my user details) and the &lt;code&gt;getMonthlyInvoice&lt;/code&gt; endpoint.&lt;/li&gt;
&lt;li&gt;On refresh? A delightful “VIBE SYNCED ✨” message in the menu. That was pure AI flair, and I loved it.&lt;/li&gt;
&lt;li&gt;The settings screen was functional and looked decent after a few rounds of UI prompting: currency selection (PHP, EUR, USD all present!), spending limits, and a refresh interval.&lt;/li&gt;
&lt;li&gt;And yes, it correctly registered itself to launch at login.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vibe Meter (Electron - TypeScript/React):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;This one was a bit further behind but definitely taking shape. It launched, and the basic React structure was there.&lt;/li&gt;
&lt;li&gt;Login was partially implemented, and the cookie capture was on its way.&lt;/li&gt;
&lt;li&gt;The settings screen existed but needed more UI love (some CSS wasn’t loading correctly when I last left it).&lt;/li&gt;
&lt;li&gt;It would need a bit more focused prompting to reach feature parity with the Mac app, but the foundation was solidly laid.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In total, the AIs probably generated something in the ballpark of 5,000 lines of Swift and around 2,000 lines of TypeScript, JavaScript (before conversion), HTML, and CSS, not to mention all the configuration files, test setups, and package manifests.&lt;/p&gt;
&lt;h3&gt;Key Takeaways &amp;amp; My Developing “Vibe Coding” Philosophy&lt;/h3&gt;
&lt;p&gt;This workshop wasn’t just about the Vibe Meter app itself; it was about demonstrating a &lt;em&gt;way&lt;/em&gt; of working, a new “vibe” for software development. Here are my big takeaways, which are constantly evolving:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AI as a Force Multiplier, Not a Replacement:&lt;/strong&gt; Let’s be clear: this isn’t about AI taking our jobs. It’s about giving us superpowers. The ability to explore ideas, build prototypes, and even flesh out significant features at 10x, 20x, or even greater speed is transformative. You become an architect, a guide, a refiner.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Becomes Cheaper, Iteration Becomes Faster:&lt;/strong&gt; This is a profound mental shift. In the past, if I spent days crafting a feature, I’d be heavily invested in that code. Ripping it out or taking a completely different approach felt costly. Now? If an AI generates a component in 20 minutes and it’s not quite right, or I have a better idea, I can tell it to refactor it, or just scrap it and try again with a new prompt. The cost of experimentation plummets. This frees you to explore more avenues.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Focus Your Energy on the Hard Problems:&lt;/strong&gt; You can delegate a huge amount of boilerplate, initial scaffolding, CRUD operations, or even well-defined, common features to the AI. This frees up your precious cognitive cycles to focus on the truly novel, complex, and differentiating aspects of your product – the parts that require deep human insight and creativity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accelerated Learning in Unfamiliar Territory:&lt;/strong&gt; This is huge for me personally. I’m primarily a native developer. But with AI, I’ve been able to dive into web technologies like TypeScript, React, and Tailwind CSS much faster than I ever could have traditionally. The AI can explain concepts, generate example code, show best practices, and help debug my newbie mistakes. It’s like having an infinitely patient tutor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The AI is Your “Brilliant but Quirky” Teammate:&lt;/strong&gt; Think of the current generation of AI tools as incredibly knowledgeable, super-fast junior-to-mid-level developers who sometimes act like autistic savants. They can produce amazing work, have access to a vast corpus of knowledge, but they can also misunderstand requirements, hallucinate APIs that don’t exist, get stuck on seemingly simple things, or rigidly adhere to an incorrect assumption. Your job is to learn their strengths, weaknesses, and how to “manage” them effectively through clear prompting and iterative feedback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt Engineering is the New Core Skill:&lt;/strong&gt; The quality of your interaction with the AI directly dictates the quality of the output. This isn’t just about typing a question; it’s about crafting clear, unambiguous specifications, providing good examples, giving constructive feedback, knowing when to break down a problem into smaller steps for the AI, and understanding how to guide its reasoning process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Horses for Courses – Choosing Your AI Tools Wisely:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google AI Studio (with Gemini):&lt;/strong&gt; My go-to for initial, comprehensive spec generation and refinement. That massive context window and less restrictive output format are invaluable for laying a strong foundation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cursor IDE:&lt;/strong&gt; Fantastic for the main development loop – iterative code generation, interactive debugging, and the ability to quickly switch between different AI models (like Gemini for speed/cost, and o3 for those really tough nuts to crack) within the context of your actual project files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code (Anthropic’s CLI tool):&lt;/strong&gt; A powerhouse for large-scale, codebase-wide refactoring (like adding linters, converting languages, or enforcing style guides). It tends to be very thorough and can run for extended periods on a single, complex task.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Power of Context Cannot Be Overstated:&lt;/strong&gt; LLMs are all about their context window. The more relevant information you can feed them – detailed specs, existing code snippets, error messages, screenshots, links to relevant documentation or GitHub repositories – the better their output will be. This is why Gemini 1.5 Pro’s 1 million token window (and even larger ones on the horizon) are such game-changers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t Be Afraid to Explore the Obscure:&lt;/strong&gt; Even if you’re working with a less common language or a niche problem, don’t assume AI can’t help. I showed a quick example of using a &lt;code&gt;Repo-to-Text&lt;/code&gt; converter tool to dump an entire Zig language GitHub repository into a flat text file, which I then uploaded to AI Studio. Gemini was then able to explain the project’s purpose, its architecture, and even answer specific questions about the Zig code, all in pirate speak for a bit of fun!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rethinking Cost vs. Value:&lt;/strong&gt; Yes, these AI tools have a cost. As I mentioned, I’m spending around $1100 a month right now between Cursor, direct API access, and Claude subscriptions. But you &lt;em&gt;have&lt;/em&gt; to weigh that against the value of your time and the sheer increase in output. If these tools allow me to build 10x or 20x more, or explore ideas that would have been too time-consuming before, the ROI is a no-brainer. Your time is valuable; AI helps you leverage it far more effectively.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Future is Now – Go Vibe with It!&lt;/h3&gt;
&lt;p&gt;This is not some far-off future; this is what’s possible &lt;em&gt;today&lt;/em&gt;. The tools are evolving at a dizzying pace. The way we conceive, design, and build software is undergoing a fundamental shift.&lt;/p&gt;
&lt;p&gt;My advice? Don’t wait. Dive in. Pick a small side project, something fun. Start experimenting with these tools. Learn how to prompt, how to iterate, how to “vibe” with your AI coding partner. See for yourself how it can transform your workflow, your learning, and your creative output.&lt;/p&gt;
&lt;p&gt;As for Vibe Meter? I’m definitely going to take the Mac app, polish it up (with more AI help, of course!), and integrate its core cost-tracking logic into my “&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://codelooper.app&quot;&gt;CodeLooper&lt;/a&gt;” project. The cycle of AI-assisted creation continues.&lt;/p&gt;
&lt;p&gt;Thanks for joining me on this live-coding adventure. Now, go build something cool!&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>MCP Best Practices</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-01-mcp-best-practices/</link>
    <guid isPermaLink="false">urn:aggr:item:5a5b1a4689b5ffb2f4282326426f6215f99fc157</guid>
    <atom:link href="https://steipete.me/posts/2025/mcp-best-practices/" rel="via" type="text/html"/>
    <pubDate>Sun, 1 Jun 2025 14:00:00 +0000</pubDate>
    <description>My comprehensive guide outlining best practices for building reliable, user-friendly Model Context Protocol (MCP) tools with proper configuration, testing, and release management.</description>
    <content:encoded>&lt;p&gt;Building high-quality Model Context Protocol (MCP) tools requires attention to detail across many dimensions. After developing several MCP tools, I’ve compiled this comprehensive guide to best practices that ensure your tools are reliable, user-friendly, and maintainable.&lt;/p&gt;
&lt;h2&gt;My MCP Tools&lt;/h2&gt;
&lt;p&gt;Here are the MCP tools I’ve built following these practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;👻 &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Peekaboo&quot;&gt;Peekaboo&lt;/a&gt;: Enables your IDE to make screenshots and ask questions about images.&lt;/li&gt;
&lt;li&gt;🤖 &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/Terminator&quot;&gt;Terminator&lt;/a&gt;: Manages a Terminal outside of the loop, so processes that might get stuck don’t break the loop.&lt;/li&gt;
&lt;li&gt;🧠 &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/claude-code-mcp&quot;&gt;Claude Code&lt;/a&gt;: A buddy for your IDE that your agent can ask if he’s stuck. Can do coding task and offer “a pair of fresh eyes” that often un-stucks the loop.&lt;/li&gt;
&lt;li&gt;🐱 &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/conduit-mcp&quot;&gt;Conduit&lt;/a&gt;: Advanced file manipulation for faster refactoring.&lt;/li&gt;
&lt;li&gt;🎯 &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://github.com/steipete/macos-automator-mcp&quot;&gt;Automator&lt;/a&gt;: AppleScript for your IDE.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;I. General Tool Configuration &amp;amp; Behavior&lt;/h2&gt;
&lt;h3&gt;Sensible Defaults&lt;/h3&gt;
&lt;p&gt;All environment variables must have sensible defaults for easy out-of-the-box usage. Users should be able to get started without extensive configuration.&lt;/p&gt;
&lt;h3&gt;Dynamic Versioning&lt;/h3&gt;
&lt;p&gt;The tool’s version is emitted in its description. This version must be read dynamically (e.g., from &lt;code&gt;package.json&lt;/code&gt;) and not hardcoded. This ensures version consistency and eliminates manual update errors.&lt;/p&gt;
&lt;h3&gt;Tool &amp;amp; Parameter Descriptions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool Titles:&lt;/strong&gt; Use descriptive, human-friendly titles for tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parameter Descriptions:&lt;/strong&gt; All parameters must offer a clear description&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optional/Required Parameters:&lt;/strong&gt; Parameters must be explicitly noted as “optional” or “required”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default Values:&lt;/strong&gt; If a parameter is optional, its default value must be explained&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These details should be verifiable by hovering over the tool in clients like Cursor or using the MCP inspector.&lt;/p&gt;
&lt;h3&gt;Parameter Parsing&lt;/h3&gt;
&lt;p&gt;Parameter parsing should be lenient (e.g., accept &lt;code&gt;path&lt;/code&gt; if &lt;code&gt;project_path&lt;/code&gt; is formally defined). Generally, advertise stricter schemas but be more lenient in execution to accommodate variations from agents.&lt;/p&gt;
&lt;h3&gt;Error Handling&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Runtime Error Handling:&lt;/strong&gt; In case of an error, emit a helpful message to the caller with information to potentially recover&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration Error Handling:&lt;/strong&gt; Misconfigurations (e.g., wrongly set environment variables) must not crash the tool. Instead, provide a useful explanation when the tool is run, enabling the user to self-correct their setup&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Output Control&lt;/h3&gt;
&lt;p&gt;There should be no output to &lt;code&gt;stdio&lt;/code&gt; during normal tool operation, as this can disrupt MCP clients. File-based logging is the designated method for operational output.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;info&lt;/code&gt; Command&lt;/h3&gt;
&lt;p&gt;At least one tool must offer an &lt;code&gt;info&lt;/code&gt; sub command. This command shall list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The version of the MCP tool&lt;/li&gt;
&lt;li&gt;The status of any required native dependencies (if applicable), including tests for their presence and functionality&lt;/li&gt;
&lt;li&gt;Any detected configuration issues or missing environment variables (e.g., problems with the logger path)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;II. Logging (Pino)&lt;/h2&gt;
&lt;h3&gt;Default File Logger&lt;/h3&gt;
&lt;p&gt;Pino is used for logging with a default file logger in the system’s log directory (e.g., &lt;code&gt;~/Library/Logs/&lt;/code&gt;). The log file path is configurable via the &lt;code&gt;[ProjectName]_LOG_FILE&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;h3&gt;Log Path Resilience&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pino logic must automatically create missing parent directories for the specified log file path&lt;/li&gt;
&lt;li&gt;If pino cannot write to the &lt;code&gt;[ProjectName]_LOG_FILE&lt;/code&gt; path, it must fall back to logging to the default temporary directory path&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Configuration&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Configurable Log Level:&lt;/strong&gt; The log level is set using the &lt;code&gt;[ProjectName]_LOG_LEVEL&lt;/code&gt; environment variable (accepts upper, lower, or mixed case values)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optional Console Logging:&lt;/strong&gt; An environment variable, &lt;code&gt;[ProjectName]_CONSOLE_LOGGING=true&lt;/code&gt;, enables logging to the console in addition to the pino file logger&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logger Flush:&lt;/strong&gt; The logger must be flushed before the process exits to ensure all log messages are written&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;III. Code, Dependencies &amp;amp; Build&lt;/h2&gt;
&lt;h3&gt;Code Quality Standards&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dependency Management:&lt;/strong&gt; All dependencies should be kept at their latest stable versions. The release script will warn for outdated dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static Analysis:&lt;/strong&gt; There must be no linter (e.g., ESLint) or TypeScript errors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File Size:&lt;/strong&gt; No single file should exceed 500 lines of code (LOC); aim for below 300 LOC&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Build Configuration&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Execution with Compiled Code:&lt;/strong&gt; The startup logic and all tool operations must always use the compiled JavaScript output (e.g., from the &lt;code&gt;dist&lt;/code&gt; folder)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shebangs:&lt;/strong&gt; Compiled JavaScript files intended for direct execution must have the correct shebang (e.g., &lt;code&gt;#!/usr/bin/env node&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NPM Package Contents:&lt;/strong&gt; The published npm package must contain only the absolute minimum files: the &lt;code&gt;dist/&lt;/code&gt; folder, any potential native components, the &lt;code&gt;README.md&lt;/code&gt;, and a &lt;code&gt;LICENSE&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IV. Testing&lt;/h2&gt;
&lt;h3&gt;Test Framework&lt;/h3&gt;
&lt;p&gt;Tests must use &lt;code&gt;vitest&lt;/code&gt; for consistency and modern testing capabilities.&lt;/p&gt;
&lt;h3&gt;Test Coverage&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TypeScript Test Suite:&lt;/strong&gt; A comprehensive test suite for the TypeScript layer is required&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-End (E2E) Tests:&lt;/strong&gt; E2E tests that validate the complete setup are necessary. These might be run as release preparation if CI execution is challenging due to permissions like macOS access&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;NPM Scripts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm run prepare-release&lt;/code&gt; executes a comprehensive test suite (detailed in Section VI)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm run inspector&lt;/code&gt; executes &lt;code&gt;npx @modelcontextprotocol/inspector node path/to/server/index.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;V. Native Binary Rules (If Applicable)&lt;/h2&gt;
&lt;h3&gt;Platform Compatibility&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;macOS Compatibility:&lt;/strong&gt; The binary must be universal (Apple Silicon &amp;amp; Intel) and support the current macOS version and the previous major version (n-1, e.g., macOS &amp;gt;= 14 if current is 15)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Optimization:&lt;/strong&gt; Compiler and linker flags must be set to achieve a minimal binary file size&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Native Code Quality&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Native Test Suite:&lt;/strong&gt; A comprehensive test suite using Swift’s native testing tools (e.g., &lt;code&gt;swift-test&lt;/code&gt; or XCTest) is required&lt;/li&gt;
&lt;li&gt;The CLI must have no linter issues (e.g., SwiftLint for Swift)&lt;/li&gt;
&lt;li&gt;A formatter must be applied (e.g., SwiftFormat for Swift)&lt;/li&gt;
&lt;li&gt;The CLI must show no analyzer issues&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Integration Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom Path Configuration:&lt;/strong&gt; An environment variable must allow setting a custom absolute path to run the native binary&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Communication:&lt;/strong&gt; If the tool uses a native library, &lt;code&gt;errno&lt;/code&gt; (or an equivalent mechanism) must be used to pass error and execution issues to the TypeScript logger and back to the tool&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version Synchronization:&lt;/strong&gt; The native CLI and the MCP tool (TypeScript package) must have the same version number. This version must be injected during the build process, not hardcoded&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Communication Protocol&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JSON Communication:&lt;/strong&gt; The native binary part of the tool must have a mode to communicate in JSON back to the TypeScript server for easier parsing. JSON responses should include debug logs if requested&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI Help Command:&lt;/strong&gt; The binary must respond to &lt;code&gt;--help&lt;/code&gt; with a helpful command explaining its use and all options&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Argument Parsing Framework:&lt;/strong&gt; The binary must use a robust argument parser framework (e.g., &lt;code&gt;swift-argument-parser&lt;/code&gt; for Swift)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Distribution&lt;/h3&gt;
&lt;p&gt;Consider options for distributing as a single, statically linked binary if feasible and beneficial for simpler installation by end-users who might use the CLI directly.&lt;/p&gt;
&lt;h2&gt;VI. Rules to Check Before a Release (&lt;code&gt;scripts/prepare-release.js&lt;/code&gt;)&lt;/h2&gt;
&lt;p&gt;There is a &lt;code&gt;scripts/prepare-release.js&lt;/code&gt; that runs an extensive test suite. The script runs these checks sequentially and stops at the first failure.&lt;/p&gt;
&lt;h3&gt;Git &amp;amp; Version Control&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Check current branch (warns if not on main or designated release branch)&lt;/li&gt;
&lt;li&gt;Check for uncommitted changes&lt;/li&gt;
&lt;li&gt;Check if synced with origin/main (or designated release branch)&lt;/li&gt;
&lt;li&gt;Version availability check (ensures version isn’t already published)&lt;/li&gt;
&lt;li&gt;Version consistency between &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Changelog Check:&lt;/strong&gt; Check for a changelog entry corresponding to the current version&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Code Quality &amp;amp; Security&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Dependency installation check&lt;/li&gt;
&lt;li&gt;Outdated dependencies check (warning only)&lt;/li&gt;
&lt;li&gt;Security audit (fails on critical/high vulnerabilities)&lt;/li&gt;
&lt;li&gt;TypeScript compilation&lt;/li&gt;
&lt;li&gt;TypeScript tests&lt;/li&gt;
&lt;li&gt;TypeScript declaration files generation&lt;/li&gt;
&lt;li&gt;Delete any build folders and reset package caches before building&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Swift analyze&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Swift formatting (SwiftFormat)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Swift linting (SwiftLint)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Swift tests&lt;/li&gt;
&lt;li&gt;No build warnings&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Binary &amp;amp; CLI Validation (If Applicable)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Swift CLI command tests (help, version, and other key functionalities)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Swift CLI error handling tests (invalid commands, missing args, invalid window index, etc.)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Swift CLI JSON output validation&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Binary exists and is executable&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Binary contains both architectures (arm64 + x86_64, verifiable via &lt;code&gt;lipo -info&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;If native binary exists:&lt;/em&gt; Binary responds correctly to &lt;code&gt;--help&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Package Validation&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Required fields in &lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Package size check (warns if &amp;gt;2MB, configurable threshold)&lt;/li&gt;
&lt;li&gt;Executable permission check in &lt;code&gt;postinstall&lt;/code&gt; (if a CLI is present)&lt;/li&gt;
&lt;li&gt;Critical files included (e.g., &lt;code&gt;dist/index.js&lt;/code&gt;, native binary name, &lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;LICENSE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;MCP server smoke test (JSON-RPC request/response)&lt;/li&gt;
&lt;li&gt;Full integration tests&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Beta Release Strategy&lt;/h3&gt;
&lt;p&gt;Releases are first done with a &lt;code&gt;beta&lt;/code&gt; tag to the npm registry so they can be tested via the &lt;code&gt;npx [packageName]@beta install&lt;/code&gt; method.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Following these best practices ensures that your MCP tools are professional, reliable, and user-friendly. They represent lessons learned from real-world development and deployment of production MCP tools. As the MCP ecosystem evolves, these practices will continue to be refined and expanded.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
  <item>
    <title>Finding My Spark Again</title>
    <link>https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-01-finding-my-spark-again/</link>
    <guid isPermaLink="false">urn:aggr:item:d0d4df61b1ec17ed4822522b4e540aa702034161</guid>
    <atom:link href="https://steipete.me/posts/2025/finding-my-spark-again/" rel="via" type="text/html"/>
    <pubDate>Sun, 1 Jun 2025 13:00:00 +0000</pubDate>
    <description>I reflect on my post-exit journey from emptiness to rediscovering my passion for building, sparked by AI&#39;s transformative potential.</description>
    <content:encoded>&lt;blockquote&gt;
&lt;p&gt;We are so back 🚀&lt;/p&gt;
&lt;p&gt;— Peter Steinberger (@steipete) &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://twitter.com/steipete/status/1925983535958999393?ref_src=twsrc%5Etfw&quot;&gt;November 20, 2024&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When I sold my shares of &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; href=&quot;https://www.nutrient.io/&quot;&gt;PSPDFKit&lt;/a&gt; after building the company and making it my identity for over 13 years, I was very broken. I’ve been pouring 200% of my time, energy, and heart’s blood into this company, and towards the end, I just felt that I needed a break. We’ve been very lucky to have found an amazing company with Insight, that acquired the majority.&lt;/p&gt;
&lt;h2&gt;The Emptiness After&lt;/h2&gt;
&lt;p&gt;For me though, I felt like I missed out on life. A lot of my normie friends had fun every weekend while I was just crushing and pushing and churning through building the best product I can possibly do. And once it fell away, there was not much left.&lt;/p&gt;
&lt;p&gt;I heard it’s common for founders to fall into a hole and to take a year off after they move on from their company. A typical company life cycle is also 4-5 years. So after 13 years in retrospect, I can see how it just needed more time to find a new purpose.&lt;/p&gt;
&lt;h2&gt;Searching for Meaning&lt;/h2&gt;
&lt;p&gt;I did a lot of stuff, I partied hard, I did plenty of therapy, I did ayahuasca, I moved to another country, I wandered around carrying this emptiness in me and hunting hedonic pleasures. And then a few months ago, something clicked. I had enough of my own bullshit, and I realized that you don’t find happiness by moving countries. You don’t find purpose. You create it.&lt;/p&gt;
&lt;h2&gt;The Spark Returns&lt;/h2&gt;
&lt;p&gt;Creating things out of ideas, building was always the thing that gave me the most joy in life. One day, things just clicked. I had a new idea, I sat on my computer and started hacking, and I realized that my spark is back.&lt;/p&gt;
&lt;p&gt;Right now, we are at an incredible crossroads in technology. AI is moving so fast and is changing the way we work in software development, but furthermore, it’s going to change the world. I haven’t been as excited, astounded, and amazed by any technology in a very long time.&lt;/p&gt;
&lt;p&gt;We are so back. It’s time to build.&lt;/p&gt;
</content:encoded>
    <category>programming</category>
  </item>
</channel>
</rss>
