<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>0xZ</title><description># cat /dev/brain &gt;&gt; posts</description><link>https://posts.oztamir.com/</link><image><url>https://posts.oztamir.com/favicon.png</url><title>0xZ</title><link>https://posts.oztamir.com/</link></image><atom:link href="https://posts.oztamir.com/rss.xml" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title>Shell in the Ghost: Migrating my blog to Astro</title><link>https://posts.oztamir.com/shell-in-the-ghost-migrating-to-astro/</link><guid isPermaLink="true">https://posts.oztamir.com/shell-in-the-ghost-migrating-to-astro/</guid><description>Why I ditched Ghost for a static Astro site, and the malware I found on the way out.</description><pubDate>Sun, 21 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Back in 2020, in the thick of COVID quarantine, I started this blog. I wrote about it in my very first post, &lt;a href=&quot;https://posts.oztamir.com/hello-world/&quot;&gt;Hello World&lt;/a&gt;: two weeks stuck at home, rediscovering how much I love messing with technology, and finally deciding to write some of it down.&lt;/p&gt;
&lt;p&gt;I needed somewhere to put the words. I picked &lt;a href=&quot;https://ghost.org&quot;&gt;Ghost&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It was, honestly, the path of least resistance. Ghost was the easy answer in 2020: spin up a VPS, install it, convert a theme into something I actually liked in an afternoon, and start typing.&lt;/p&gt;
&lt;p&gt;And for years, it delivered. It got out of my way. I wrote, it published, the thing just worked.&lt;/p&gt;
&lt;p&gt;But over the past few months, something shifted. A string of nasty exploits started landing for Ghost. Serious ones, the kind that let a stranger &lt;a href=&quot;https://www.sentinelone.com/vulnerability-database/cve-2026-26980/&quot;&gt;read your entire database&lt;/a&gt; or &lt;a href=&quot;https://www.endorlabs.com/learn/rce-in-ghost-cms-ghsa-cgc2-rcrh-qr5x&quot;&gt;run their own code on your server&lt;/a&gt;.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;updating-ghost.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;A terminal mid Ghost update: sudo systemctl commands and ghost service status showing the blog service active and running.&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;My last-ever Ghost update.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;And at some point, mid-update for the umpteenth time, I just stopped and asked myself the obvious question:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Why is my blog running on a CMS in this day and age?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I mean, really. It’s a stack of articles that never change after I hit publish. There’s no reason for it to be a living, internet-facing server that I rent, patch, and pray over. I could just… vibe-code a static site.&lt;/p&gt;
&lt;p&gt;A whole class of these problems evaporates the second there’s no server to attack and no database to read. And going static would come with a pile of other benefits I’d been wanting anyway:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No server to babysit.&lt;/strong&gt; A blog is text. It should be a folder of files a CDN hands out: nothing to log into, nothing to exploit, nothing to patch at 11pm.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hosting I’m already using.&lt;/strong&gt; I’ve got a bunch of projects running on Cloudflare already, and the experience has been fantastic: fast, painless, and costing me exactly nothing. Meanwhile I was paying for a VPS just to keep Ghost alive. It wasn’t a lot of money, but it had started to feel like a bill I had no business still paying.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total control.&lt;/strong&gt; My markup, my styles, my routes, my rules. If I want the blog to do something weird, it should do something weird.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Markdown for the robots.&lt;/strong&gt; I wanted agents (crawlers, LLMs, whatever) to be able to ask for a post and get clean Markdown back instead of a soup of HTML. Ghost &lt;a href=&quot;https://forum.ghost.org/t/ghost-posts-to-markdown-for-ai-bots-and-others/62135&quot;&gt;flat-out can’t do this&lt;/a&gt;, and it bugged me more than it probably should have.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A modern stack.&lt;/strong&gt; I wanted to build on the tools I actually enjoy using now (&lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, &lt;a href=&quot;https://tailwindcss.com&quot;&gt;Tailwind&lt;/a&gt;, the modern web) instead of living inside Ghost’s theme world. (And sure, open-sourcing the result on &lt;a href=&quot;https://github.com/OzTamir/posts&quot;&gt;GitHub&lt;/a&gt; was a nice bonus, but that was a side perk, not the point.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’d been circling this idea for a while without actually doing anything about it. What finally tipped me over wasn’t the blog at all. It was a side-quest.&lt;/p&gt;
&lt;p&gt;Last week I built myself &lt;a href=&quot;https://talks.oztamir.com&quot;&gt;a new site&lt;/a&gt; to host and share my public-speaking stuff.&lt;/p&gt;
&lt;p&gt;I had a fun design session with Claude, and once that was live, I thought - why not do the same for my homepage? So I went ahead and redesigned it as well.&lt;/p&gt;
&lt;p&gt;And at that point, I was on a roll - and I thought: “well, now’s as good a time as any to finally do that blog migration I keep postponing.”&lt;/p&gt;
&lt;p&gt;So I did.&lt;/p&gt;
&lt;h2 id=&quot;choosing-the-weapon&quot;&gt;Choosing the weapon&lt;/h2&gt;
&lt;p&gt;First, the boring-but-important question: what do you build a blog out of?&lt;/p&gt;
&lt;p&gt;Three real contenders:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://gohugo.io&quot;&gt;Hugo&lt;/a&gt;.&lt;/strong&gt; The old reliable. Fast, battle-tested, boring in the good way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A vibe-coded, self-made generator.&lt;/strong&gt; Roll my own from scratch. Maximum control, maximum yak-shaving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;.&lt;/strong&gt; Component-based, content collections built in, ships zero JS by default.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went back and forth longer than I’d like to admit. What tipped it was &lt;a href=&quot;https://www.millerdatabases.com/migrating-from-ghost-to-astro-with-claude-code/&quot;&gt;this write-up&lt;/a&gt; on migrating from Ghost to Astro with Claude Code. It walked almost exactly the path I was about to take. Astro it was.&lt;/p&gt;
&lt;p&gt;So now it was time to actually build the thing, and, frankly, it wasn’t much to write home about. I fired up Claude, wrote a careful spec, and set it running.&lt;/p&gt;
&lt;p&gt;One choice that mattered: I asked it to operate as an orchestrator and lean heavily on sub-agents. And the single most important instruction I gave, the thing that kept the output high-quality, was that it should &lt;strong&gt;not port any of Ghost’s code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It had to rebuild the behavior natively, as if the site had always been Astro. That turned out to be the challenging part: Ghost CSS styles and other cruft kept sneaking back in, and it took a long session with Claude before we’d scrubbed all of it out.&lt;/p&gt;
&lt;p&gt;There were also some decisions around JavaScript. I built the site to have fully static output - by default, Astro ships no JavaScript to the client. But I did want a little interactivity: for example, the “load more” button on the main page.&lt;/p&gt;
&lt;p&gt;Luckily, Astro has a feature called &lt;a href=&quot;https://docs.astro.build/en/concepts/islands/&quot;&gt;islands&lt;/a&gt;: tiny bits of JavaScript, shipped only where needed, which let the site eat the cake and have it too.&lt;/p&gt;
&lt;p&gt;Beyond that it was a long tail of small details: fixing some styles, swapping out a few broken fonts, making sure none of my assets were too heavy, and (while I was at it) renaming them from &lt;code&gt;Screenshot 2021…&lt;/code&gt; and &lt;code&gt;WhatsApp Web&lt;/code&gt; into something readable.&lt;/p&gt;
&lt;p&gt;Finally, I built a set of Astro MDX components to hold all the various elements my posts rely on: figures, videos, embedded tweets, and the like.&lt;/p&gt;
&lt;p&gt;The result builds to a folder of static files that Cloudflare serves on every push. No server, no database, nothing to patch.&lt;/p&gt;
&lt;h2 id=&quot;plot-twist-i-was-already-haunted&quot;&gt;Plot twist: I was already haunted&lt;/h2&gt;
&lt;p&gt;Partway through the migration, while Claude was going through my old Ghost export to port the posts over, it flagged something: every single one of my 41 posts had the same obfuscated &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; tucked into its footer, not in the post bodies but in Ghost’s per-post “code injection” field.&lt;/p&gt;
&lt;p&gt;It was base64-encoded. When I decoded it, it pointed at a domain I’d never seen, pulling a remote script onto my blog for anyone who visited. It was also built to be quiet (it fired only once per visitor), which is part of why I’d never caught it myself.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;claude-malware-alert.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Claude&amp;#x27;s security finding: every one of the 41 posts has an identical obfuscated codeinjection_foot that decodes to malware, injecting a remote script on every page.&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;Not the way you want to find out.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;My blog, the one I was in the middle of leaving &lt;em&gt;because&lt;/em&gt; Ghost sites were under attack, had already been compromised.&lt;/p&gt;
&lt;p&gt;What are the odds, right?&lt;/p&gt;
&lt;p&gt;But once the irony wore off, it stopped being funny. This wasn’t a defaced homepage or a cosmetic prank. Code I hadn’t written was running in my readers’ browsers.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;malware-payload.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;The stored malware: an obfuscated, self-executing script that base64-decodes a URL and injects it as a remote script, guarded by a localStorage flag so it runs once per visitor.&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;Sitting in the footer of every single post.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;So I sat down and did some research with Claude, and what I found made it worse, not better: this wasn’t personal. My blog hadn’t been singled out. It had been swept up in a &lt;a href=&quot;https://blog.xlab.qianxin.com/ghost-cms-mass-compromised-via-cve-2026-26980-now-fueling-clickfix-attacks/&quot;&gt;broad, automated campaign&lt;/a&gt; that hit hundreds of Ghost sites, Oxford and Harvard among them. The injected script was a loader for a &lt;a href=&quot;https://guard.io/labs/captchageddon-unmasking-the-viral-evolution-of-the-clickfix-browser-based-threat&quot;&gt;&lt;strong&gt;ClickFix&lt;/strong&gt;&lt;/a&gt; attack: the kind that shows a visitor a fake “verify you’re human” box and tricks them into pasting a command that quietly installs malware on their own machine.&lt;/p&gt;
&lt;p&gt;For some stretch of time, a real person could have landed on one of my posts and walked away infected, because of my blog. I genuinely, deeply hope nobody did. If you ever visited the site and saw a prompt telling you to press Win+R and paste something to “verify” yourself: don’t, and run a scan.&lt;/p&gt;
&lt;p&gt;Even though I was only hours away from replacing the whole site with the clean static version, I knew that waiting it out was not an option. So I stopped everything, went into the live Ghost install, and stripped the malware out of all 41 posts by hand, right then.&lt;/p&gt;
&lt;p&gt;If anything, finding this on the way out didn’t shake my decision to migrate. It validated it. The whole problem traces back to one thing: a server. Ghost is software, running on a machine, exposed to the internet, and software running on a server is something that can be attacked and compromised.&lt;/p&gt;
&lt;p&gt;A static site has none of that. There’s no application to exploit and no database to read: just plain web pages sitting in storage, handed out exactly as they are. No server means no attack surface, and with no attack surface there’s nothing there to compromise in the first place.&lt;/p&gt;
&lt;h2 id=&quot;a-blog-the-robots-can-read&quot;&gt;A blog the robots can read&lt;/h2&gt;
&lt;p&gt;Remember the “Markdown for the robots” bullet from way back at the top? This is where it pays off, and it’s the part I’m most pleased with.&lt;/p&gt;
&lt;p&gt;Quick context. There’s a growing push to make websites legible to AI agents: not just human browsers, but the crawlers and LLMs that increasingly read the web on our behalf.&lt;/p&gt;
&lt;p&gt;Cloudflare actually wrote up a good framing of this they call &lt;a href=&quot;https://blog.cloudflare.com/agent-readiness/&quot;&gt;“agent readiness”&lt;/a&gt;: the idea that a modern site should present itself cleanly to machines, not only people. There’s even a checker, &lt;a href=&quot;https://isitagentready.com&quot;&gt;isitagentready.com&lt;/a&gt;, that audits your site against a pile of emerging standards.&lt;/p&gt;
&lt;p&gt;I ran my freshly-migrated blog through it and, naturally, set out to turn every red X green.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;agent-readiness-score.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Agent-readiness scan of posts.oztamir.com: overall score 50, Level 4 &amp;#x27;Agent-Integrated&amp;#x27;, with Discoverability 75, Content 100, Bot Access Control 100, API/Auth/MCP/Skill Discovery 14, and Commerce not checked.&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;Where I landed: ‘Agent-Integrated.’&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;A few were easy, static wins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://llmstxt.org&quot;&gt;llms.txt&lt;/a&gt;&lt;/strong&gt; is a plain-text index of the whole blog (every post, newest first, with excerpts), &lt;a href=&quot;https://github.com/OzTamir/posts/blob/main/src/pages/llms.txt.ts&quot;&gt;generated&lt;/a&gt; from the same content collection that powers the RSS feed, so it can never drift out of sync.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content signals in &lt;a href=&quot;https://github.com/OzTamir/posts/blob/main/src/pages/robots.txt.ts&quot;&gt;&lt;code&gt;robots.txt&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; explicitly tell crawlers what they may do: yes to search, yes to being cited in AI answers, &lt;em&gt;no&lt;/em&gt; to training models on my writing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discovery headers&lt;/strong&gt;: an &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8288&quot;&gt;RFC 8288&lt;/a&gt; &lt;a href=&quot;https://github.com/OzTamir/posts/blob/main/public/_headers&quot;&gt;&lt;code&gt;Link&lt;/code&gt; header&lt;/a&gt; and a &lt;a href=&quot;https://github.com/OzTamir/posts/blob/main/public/.well-known/api-catalog&quot;&gt;&lt;code&gt;.well-known&lt;/code&gt; API catalog&lt;/a&gt; pointing agents at the endpoints that actually exist (&lt;code&gt;/posts.json&lt;/code&gt;, the sitemap, the RSS feed) and nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The content signals are my favorite of the bunch, three lines that just say what I want out loud:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;User-agent: *&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Content-Signal: search=yes, ai-input=yes, ai-train=no&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Allow: /&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Search engines, welcome. Cite me in AI answers, sure. Train a model on my writing? No thanks. It’s advisory, not enforcement. But it’s nice to be able to state the preference at all.&lt;/p&gt;
&lt;p&gt;That “nothing else” matters to me. A lot of the checklist is about advertising APIs, OAuth flows, and tool endpoints, none of which a static blog has. The honest move isn’t to publish that metadata anyway; pointing an agent at endpoints that don’t exist is worse than staying quiet. So I shipped what’s real and skipped what isn’t.&lt;/p&gt;
&lt;p&gt;But the one I actually cared about was &lt;strong&gt;serving Markdown.&lt;/strong&gt; The idea: when an agent asks for a post with an &lt;code&gt;Accept: text/markdown&lt;/code&gt; header, hand it clean Markdown instead of rendered HTML. Browsers still get the pretty page; machines get the source. This is the exact thing Ghost couldn’t do, and the reason it’d been nagging at me for months.&lt;/p&gt;
&lt;p&gt;Funny enough, Cloudflare &lt;em&gt;does&lt;/em&gt; have a native “Markdown for Agents” feature that does precisely this: flip a switch, done. Except it’s gated behind their paid plans, and my little blog is very much on the free one. So the built-in path was out. Fine. I’d build it myself.&lt;/p&gt;
&lt;p&gt;So how are we doing it? The first part is a generator that &lt;a href=&quot;https://github.com/OzTamir/posts/blob/main/src/pages/%5Bslug%5D.md.ts&quot;&gt;emits a &lt;code&gt;.md&lt;/code&gt; version of every post&lt;/a&gt; straight from the source at build time (&lt;a href=&quot;https://github.com/OzTamir/posts/blob/main/src/utils/post-markdown.ts&quot;&gt;resolving image and video URLs&lt;/a&gt; to their real paths along the way). The second part is a tiny &lt;a href=&quot;https://github.com/OzTamir/posts/blob/main/worker/index.ts&quot;&gt;Cloudflare Worker&lt;/a&gt; (all of 2 KB) that sits in front of the static files, checks the &lt;code&gt;Accept&lt;/code&gt; header, and serves the Markdown twin to anything that asks for it. Everyone else gets HTML, untouched. The whole interesting bit is about a dozen lines:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#616E88&quot;&gt;// Only HTML page routes reach this Worker. If the client prefers Markdown,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#616E88&quot;&gt;// serve the build-time .md sibling; everyone else gets HTML, untouched.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;mdPath&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; prefersMarkdown&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;headers&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Accept&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;))) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; md&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; env&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;ASSETS&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;mdPath&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; url&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;))&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;md&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;ok&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; md&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;      headers&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Content-Type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;text/markdown; charset=utf-8&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; Vary&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Accept&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#616E88&quot;&gt;  // No Markdown variant for this page. Fall through to HTML.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; env&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;ASSETS&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The whole thing weighs nothing, costs nothing, and does something my old CMS fundamentally couldn’t. If you’re an agent (or just curious), you can watch it work:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; -H&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Accept: text/markdown&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; https://posts.oztamir.com/shell-in-the-ghost-migrating-to-astro/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll get this very post back as clean Markdown. Open the same URL in a browser and you’ll get the page you’re reading now. One URL, two representations, picked by who’s asking.&lt;/p&gt;
&lt;p&gt;That’s the kind of thing “total control” was supposed to buy me. Turns out it did.&lt;/p&gt;
&lt;h2 id=&quot;so-was-it-worth-it&quot;&gt;So, was it worth it?&lt;/h2&gt;
&lt;p&gt;A weekend of work to replace something that already worked fine. No new features my readers will notice. Same posts, same URLs, same font (okay, for the first time, &lt;em&gt;actually&lt;/em&gt; the right font).&lt;/p&gt;
&lt;p&gt;But that was never the point.&lt;/p&gt;
&lt;p&gt;What I came away with is a blog with nothing to patch and no server for anyone to break into, that costs me nothing to run, and that I have complete control over.&lt;/p&gt;
&lt;p&gt;For years, the common wisdom has been that hand-rolling your own version of popular services is almost never the efficient call. There’s always something off-the-shelf that’ll get you most of the way for a fraction of the effort.&lt;/p&gt;
&lt;p&gt;But with coding agents, that math changes. Building exactly what you want is suddenly cheap enough to be worth it. And what you get for the effort is the one thing no off-the-shelf product can hand you: a blog that does exactly what you want, because you’re the one who made it do that. That’s the part that makes it &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;My blog used to be something I rented. Now it’s something I own, and I can get back to the only part I actually cared about in the first place: writing.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><category>Security</category><category>Technical</category><category>Astro</category><category>Cloudflare</category></item><item><title>Let&apos;s write a harness (or: Harness Engineering 101)</title><link>https://posts.oztamir.com/lets-write-a-harness-or-harness-engineering-101/</link><guid isPermaLink="true">https://posts.oztamir.com/lets-write-a-harness-or-harness-engineering-101/</guid><description>The same model sits behind Cursor, Claude Code, and most other agents. What makes them different isn&apos;t the brain - it&apos;s the harness. So let&apos;s write one.</description><pubDate>Sat, 06 Jun 2026 12:57:11 GMT</pubDate><content:encoded>&lt;p&gt;I was sitting with a few colleagues a while back - the kind of engineers who make you feel slow in a code review - and we got to talking about coding agents. Which one’s worth paying for, where they fall on their face, the usual.&lt;/p&gt;
&lt;p&gt;Somewhere in there one of them said, more thinking out loud than asking, “honestly I’m not sure how the agent even picks what to read. Does it just get the whole repo?”&lt;/p&gt;
&lt;p&gt;Nobody answered. We moved on.&lt;/p&gt;
&lt;p&gt;But it nagged me. Look at everything packed into that one shrug. Something reads the file off the disk. Something decides which file, how much of it, and whether reading a file is even the right move right now. Something holds onto what you said three turns ago so it’s still around on the next one. That’s a pile of work and a pile of decisions - and all of it is plain code that somebody sat down and wrote.&lt;/p&gt;
&lt;p&gt;That code has a name. It’s the harness. And the people leaning on it every single day had never looked inside it.&lt;/p&gt;
&lt;p&gt;What got me is how boring it is in there. No secret sauce, no orchestration trick the Cursor or Claude Code people figured out that the rest of us are just renting. A harness is an HTTP POST in a while loop: send the model some text, get some back, run whatever it asked for, go around again. That’s not me simplifying for the blog. That’s the shape of the whole thing.&lt;/p&gt;
&lt;p&gt;So I ran a workshop for my team and we built one from scratch, together, in an afternoon. This is that workshop, on the page.&lt;/p&gt;
&lt;p&gt;We’ll start with an empty file and end with a working coding agent - every line that matters, nothing hand-waved. By the last section, the only thing between you and your own harness is the time it takes to type it out.&lt;/p&gt;
&lt;p&gt;No magic. Let me show you.&lt;/p&gt;
&lt;h2 id=&quot;agent--model--harness&quot;&gt;Agent = Model × Harness&lt;/h2&gt;
&lt;p&gt;Before any code, one piece of vocabulary - and it’s worth borrowing the real-world meaning, because it’s the whole point.&lt;/p&gt;
&lt;p&gt;A harness, the original kind, is the rig of straps you put on a horse. It doesn’t make the animal any stronger - it gives you reins, so all that power goes where you want it. Without it you’ve got a very capable animal and no way to steer.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;saddled-horse-arena.jpg&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;An agentic harness is the same idea, pointed at a model instead of a horse.&lt;/p&gt;
&lt;p&gt;The model is the power - it can write code, reason about a bug, plan a refactor. But on its own it’s a horse with no reins: text in, text out, and that’s the end of its reach. It can’t open your files, run your tests, or remember what you said a minute ago. The harness is everything we strap around it to fix that.&lt;/p&gt;
&lt;p&gt;Concretely, it’s the code that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;feeds the model context&lt;/strong&gt; - the system prompt, your files, the conversation so far&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;runs the tools the model asks for&lt;/strong&gt; - reading a file, running a command, searching the web&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;remembers&lt;/strong&gt; - holding the whole back-and-forth so the model isn’t a goldfish&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;closes the loop&lt;/strong&gt; - taking what a tool returned and handing it back to the model to react to&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;enforces the limits&lt;/strong&gt; - deciding what the model is and isn’t allowed to touch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that is the model. All of it is plain code we’re about to write.&lt;/p&gt;
&lt;p&gt;So the agent you use every day is both halves together: &lt;code&gt;Model × Harness&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That’s why the same model feels like a different tool depending on where you meet it. It’s the same &lt;code&gt;Claude Opus 4.8&lt;/code&gt; behind Cursor, behind Claude Code, behind OpenCode - same brain every time. What changes is the rig around it. The harness is the part you can shape, which makes it the part worth understanding.&lt;/p&gt;
&lt;p&gt;So let’s go build one. From the most boring possible starting point.&lt;/p&gt;
&lt;h2 id=&quot;a-repl-with-a-borrowed-brain&quot;&gt;A REPL with a borrowed brain&lt;/h2&gt;
&lt;p&gt;For the purpose of the story, let’s say we are the maintainers of a tiny open source Python library called &lt;code&gt;slugkit&lt;/code&gt; with one function, &lt;code&gt;slugify&lt;/code&gt;, that’s supposed to turn &lt;code&gt;&quot;Hello, World!&quot;&lt;/code&gt; into a clean URL slug.&lt;/p&gt;
&lt;p&gt;Someone filed a bug: it’s spitting out &lt;code&gt;hello--world&lt;/code&gt; with a double hyphen instead of &lt;code&gt;hello-world&lt;/code&gt;. Our goal is to build an harness that can fix that bug.&lt;/p&gt;
&lt;p&gt;We start, of course, with crafting our “brain” - a function that uses &lt;a href=&quot;https://platform.claude.com/docs/en/api/messages&quot;&gt;Anthropic’s Messages API&lt;/a&gt;to allow us to make inference calls to the LLM:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; requests&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;post&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;https://api.anthropic.com/v1/messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;        headers&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;x-api-key&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; ANTHROPIC_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;                 &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;anthropic-version&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;2023-06-01&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;        json&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;claude-opus-4-8&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;max_tokens&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 4096&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;              &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;system&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tools&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    ).&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the model. You POST some messages, you get a response. There’s no state hiding in there, no session, no memory - every call is a blank slate. Hold onto that, because half of what the harness does is paper over it.&lt;/p&gt;
&lt;p&gt;Wrap it in a loop and you’ve got the world’s dumbest REPL:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    user_input &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    reply &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;([{&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; user_input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;reply&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s give it the job:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; fix slugify&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sure! Here&apos;s a haiku about garden slugs:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  a silver trail glints -&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  the slow wanderer at dawn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  leaves its mark, then gone.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…okay. In fairness, I said “fix slugify” to a thing that has no idea it’s a coding agent. It’s a general-purpose chatbot, “slugify” reads like a cute writing prompt, and we never told it otherwise.&lt;/p&gt;
&lt;h3 id=&quot;step-1-adding-a-system-prompt&quot;&gt;Step 1: Adding a system prompt&lt;/h3&gt;
&lt;p&gt;So let’s tell it. The API has a &lt;code&gt;system&lt;/code&gt; parameter that sits outside the conversation and allows us to specify the &lt;strong&gt;system prompt&lt;/strong&gt; for our little agent - a set of foundational instructions given to an AI model that defines its role, behavior, tone, and constraints. It acts as the AI’s “job description,” guiding how it should respond to all subsequent user requests.&lt;/p&gt;
&lt;p&gt;Here’s the system prompt that we will provide our little agent with:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;SYSTEM &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;You are a coding agent.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Make small, focused edits.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Prefer existing project patterns.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Run tests after every change.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Ask before anything destructive.&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;reply &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;SYSTEM&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it again:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; fix the slugify bug&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Happy to help - which file is it in?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; src/text_utils.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Thanks! So - what are we working on today?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It knows it’s a coding agent now. It asked the right question. There’s only one, tiny issue - it has no memory! That’s the blank slate biting us. Remember - each &lt;code&gt;model()&lt;/code&gt; call stands alone - the second time around, the agent has no clue the first exchange ever happened. It’s a goldfish.&lt;/p&gt;
&lt;h3 id=&quot;step-2-adding-conversational-history&quot;&gt;Step 2: Adding conversational history&lt;/h3&gt;
&lt;p&gt;The fix is almost insultingly simple: keep the conversation in a list, and replay the whole thing every turn.&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; []&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;                                          # the harness&apos;s memory&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    user_input &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; user_input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    reply &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;SYSTEM&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    text &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; reply&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;assistant&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; text&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;    print&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There’s the “memory.” It’s a list. Nobody’s keeping your conversation in some clever store in the server - the harness hangs onto every message and ships the entire transcript back, top to bottom, on every call. The model re-reads the whole thing each time and picks up where it left off.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;context-window-turns-diagram.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;It’s also why a long session gets slow and pricey, and why the agent eventually “forgets” something you said an hour ago. It’s all riding in that one list, and the list has a ceiling. More on that later.&lt;/p&gt;
&lt;p&gt;For now: the agent has a job and a memory. It still can’t touch a single file.&lt;/p&gt;
&lt;h2 id=&quot;the-model-is-sealed-in-a-box&quot;&gt;The model is sealed in a box&lt;/h2&gt;
&lt;p&gt;Here’s the thing that took me longest to internalize: the model genuinely cannot do anything except produce text. It’s like a brain in a jar - it can think, but without the harness it cannot see or interact with the real world.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;brain-in-jar-robot.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Which means tools aren’t a feature we’re bolting on for power. They’re the model’s only window onto the world. No tools, no eyes.&lt;/p&gt;
&lt;h3 id=&quot;step-3-adding-tools&quot;&gt;Step 3: Adding tools&lt;/h3&gt;
&lt;p&gt;So let’s give it one. Reading a file seems like a reasonable first verb:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#D08770&quot;&gt;tool&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;   # builds the JSON schema from the function signature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; offset&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; limit&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;120&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Read a line-numbered window of a file.&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    lines &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; open&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;read&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;splitlines&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    window &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; lines&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;offset &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; offset &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; limit&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;offset &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; i &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;:&gt;4&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; | &lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;ln&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;                     for&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; i&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; ln &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; enumerate&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;TOOLS &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, tools are just ordinary Python functions - no LLM magic, everything deterministic.&lt;/p&gt;
&lt;p&gt;The interesting part is what the &lt;code&gt;@tool&lt;/code&gt; decorator does with it (and again, this is just a sample code - the code itself is not the point, it’s the concept that matters): it reads the signature and docstring and turns them into a plain JSON object - a contract the model can actually read. That object &lt;em&gt;is&lt;/em&gt; the tool, as far as the model is concerned:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;                              #&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; what&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; the&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; calls&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Read a line-numbered window.&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    #&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; WHEN&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; reach&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;input_schema&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;                                 #&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; WHAT&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; it&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; must&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; hand&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; over&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;object&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;properties&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;   {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;                       &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;offset&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;integer&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;                       &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;limit&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;integer&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;required&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four things: a name to call, a description telling the model &lt;em&gt;when&lt;/em&gt; this is the right move, an input schema spelling out &lt;em&gt;what&lt;/em&gt; arguments it can pass, and (back in our code, never sent) the actual function that runs. The model only ever sees the first three. It has no idea there’s a Python function on the other end - it just knows a verb exists and what shape the arguments take.&lt;/p&gt;
&lt;p&gt;Now watch what actually goes over the wire. We send the conversation plus the tool contract:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;POST /v&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;/messages&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;claude-opus-4-8&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;fix the slugify bug&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;tools&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; ...&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the model answers - not with text, but with a request to use the tool:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;stop_reason&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;toolu_01abc&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;src/text_utils.py&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;offset&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;limit&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 120&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  }]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the harness sees this &lt;code&gt;tool_use&lt;/code&gt; request, it calls a dispatch function that resolves the tool &lt;code&gt;id&lt;/code&gt; to a handler function and runs it:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;TOOL_HANDLERS &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;edit_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; edit_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;bash&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; bash&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; dispatch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;block&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;                       # one tool request from the model&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; args &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; block&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;],&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; block&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        handler &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; TOOL_HANDLERS&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;      # the model only handed us a name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        output &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; str&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;handler&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;))&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;      # plain Python, running on your machine&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    except&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; e&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        output &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&quot;Error: &lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;__name__&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_result&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; block&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;],&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;        # ties the answer back to the question&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; output&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the tool result in hand, the harness then sends the result back as the next message - wrapped so the model knows which request it’s answering:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;POST /v&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;/messages&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;fix the slugify bug&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;assistant&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt; /* the tool_use block from above */&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; ]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_result&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;tool_use_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;toolu_01abc&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;   1 | import re&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;   2 | def slugify(text):&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;   3 | ...&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }]}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;tools&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; ...&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tool’s output goes with a &lt;code&gt;tool_use_id&lt;/code&gt; that matches the &lt;code&gt;id&lt;/code&gt; from its request, so it can line the result up with the right ask when it fired off several at once. Then the model gets the whole transcript again, sees the file it wanted, and decides what to do next.&lt;/p&gt;
&lt;p&gt;That’s the whole mechanism. The &lt;code&gt;stop_reason&lt;/code&gt; flips to &lt;code&gt;tool_use&lt;/code&gt;, and the model hands back a name and a bag of arguments that match the schema we sent. It didn’t read anything. It can’t. It asked us to, and then it stopped and waited - because running the thing is our job, not its.&lt;/p&gt;
&lt;p&gt;Which is the loop. Here’s the whole agent, every line:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;                                   # one turn per thing the user types&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    while&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;                               # the agent loop&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;SYSTEM&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;TOOLS&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;assistant&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;stop_reason&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; !=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;            print&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;text_from&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]))&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;   # no tool wanted - just answer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;            break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        results &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;dispatch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;                   if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; b&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ==&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;role&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; results&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read it slowly, because that inner loop is the entire game&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We send the conversation and the tools.&lt;/li&gt;
&lt;li&gt;The model either answers in plain text - in which case we print it and go back to waiting for the user - or it asks for a tool.&lt;/li&gt;
&lt;li&gt;If it asks, we run the tool, drop the result into the conversation, and loop straight back to the model without stopping for input.&lt;/li&gt;
&lt;li&gt;The model sees the result, decides what’s next, and around it goes.&lt;/li&gt;
&lt;li&gt;Read, run, observe, repeat, until it’s got what it needs and finally just talks to you.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;agent-loop-sequence-diagram.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;That’s a complete coding agent. After we added some more tools (&lt;code&gt;ls&lt;/code&gt; tool, &lt;code&gt;write_file&lt;/code&gt; tool, and some other basic ones) we can try to give it the bug again:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; fix the slugify bug in src/text_utils.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → read_file(&quot;src/text_utils.py&quot;, 0, 40)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Found it: re.sub(r&quot;\s&quot;, &quot;-&quot;, text) replaces each whitespace character on its&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  own, so &quot;Hello, World!&quot; - which has a comma AND a space - becomes &quot;hello--world&quot;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → write_file(&quot;tests/test_slugify.py&quot;, ...)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Added a failing test: slugify(&quot;Hello, World!&quot;) should be &quot;hello-world&quot;.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It read the file. It actually found the bug - the regex matches single whitespace characters, so two in a row produce two hyphens. It wrote a test to prove it. With about forty lines of harness, the thing genuinely works.&lt;/p&gt;
&lt;p&gt;Then it says:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Now let me run the test to confirm the fix...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → ??? there&apos;s no tool for that&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  I can&apos;t actually execute anything - could you run `pytest -q` and paste the output?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ah. It can read files and write them, but we never gave it a way to &lt;em&gt;run&lt;/em&gt; anything. It’s stuck asking me to be its hands.&lt;/p&gt;
&lt;p&gt;We could fix that by adding a tool for every command under the sun - one for &lt;code&gt;pytest&lt;/code&gt;, one for &lt;code&gt;git&lt;/code&gt;, one for &lt;code&gt;grep&lt;/code&gt;, on and on forever. Or we could give it the one tool that already runs every command there is.&lt;/p&gt;
&lt;h2 id=&quot;so-we-handed-it-a-shell&quot;&gt;So we handed it a shell&lt;/h2&gt;
&lt;p&gt;The “tool for everything” plan falls apart the second you write it down. You’d need &lt;code&gt;pytest&lt;/code&gt; and &lt;code&gt;ls&lt;/code&gt; and &lt;code&gt;cat&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;mkdir&lt;/code&gt; and &lt;code&gt;mv&lt;/code&gt; and &lt;code&gt;git status&lt;/code&gt; and &lt;code&gt;git diff&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt; and &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;wc&lt;/code&gt; and… In the deck I gave for the workshop, had a slide that was just forty of these in tiny type, and the joke is that it isn’t even close to all of them.&lt;/p&gt;
&lt;p&gt;You’d be writing tool wrappers until the heat death of the universe, and the agent would still hit the one command you forgot.&lt;/p&gt;
&lt;h3 id=&quot;step-4-command-execution&quot;&gt;Step 4: Command Execution&lt;/h3&gt;
&lt;p&gt;Luckily, there’s already a program that runs every command there is. It’s the shell. So why don’t we just give the agent &lt;code&gt;bash&lt;/code&gt;?&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#D08770&quot;&gt;tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; bash&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; cwd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; timeout&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    &quot;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Run a shell command. Truncate output to the last 8 KB.&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    proc &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; subprocess&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; cwd&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;cwd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; timeout&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;timeout&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; capture_output&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; tail&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;proc&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;stdout&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 8&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 1024&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;TOOLS &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; edit_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; bash&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One tool. Now the model can run anything - &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, a one-liner it makes up on the spot - and we don’t have to anticipate a thing. The &lt;code&gt;tail&lt;/code&gt; is the only bit of taste in there: command output can be enormous, and we don’t want a &lt;code&gt;find /&lt;/code&gt; dumping a megabyte into the conversation, so we keep the last 8 KB and toss the rest.&lt;/p&gt;
&lt;p&gt;There’s only one tiny problem - we just gave a language model an unsupervised shell on our machine. It can run whatever it wants - including &lt;code&gt;rm -rf&lt;/code&gt;, including &lt;code&gt;curl something-sketchy | sh&lt;/code&gt;, including things neither of us thought of. The model is usually trying to help. “Usually” is not a security model.&lt;/p&gt;
&lt;h3 id=&quot;step-5-sandboxing&quot;&gt;Step 5: Sandboxing&lt;/h3&gt;
&lt;p&gt;But it’s not all bad. Remember - the model can only see and interact with the world through the lenses that the harness gives it. If we want, we can just give it lenses that, for example, can’t read your &lt;code&gt;.env&lt;/code&gt; files! Or ones that prevents it from running dangerous commands!&lt;/p&gt;
&lt;p&gt;The dumbest version that does anything is an allowlist and a denylist:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;ALLOWED &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;pytest&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;ruff&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;git diff&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;git status&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;ls&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;DENIED  &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;rm\s&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;-rf&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;curl.&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\|.&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;sh&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\bnc\b&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; bash&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; cwd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; timeout&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;startswith&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; p &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; ALLOWED&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; SandboxError&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;not in allowlist&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;re&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;search&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; cmd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; p &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; DENIED&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; SandboxError&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;matches deny pattern&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; subprocess&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;cmd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; cwd&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;cwd&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; timeout&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;timeout&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; capture_output&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every command the model wants now has to pass through us first - allow it, refuse it, or rewrite it - before a single byte hits the shell. That’s the real shape of a sandbox: the harness sits between the model and the machine and gets the final say.&lt;/p&gt;
&lt;p&gt;I want to be honest about what this is, though: this is not nearly good enough, and it will not save you. The model can write &lt;code&gt;pytest; rm -rf ~&lt;/code&gt; and sail straight past &lt;code&gt;startswith(&quot;pytest&quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Real isolation happens a level down - a locked-down process, a throwaway container, a filesystem the agent can’t escape - not a regex hoping to outguess a model that’s better at strings than you are.&lt;/p&gt;
&lt;p&gt;Back to our &lt;code&gt;slugify&lt;/code&gt; bug, the agent now has &lt;code&gt;bash&lt;/code&gt; wired up, and it can finally close its own loop:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; fix the slugify bug in src/text_utils.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → read_file(...)        found the regex&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → edit_file(...)        \s → \s+&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → bash(&quot;pytest -q&quot;)     14 passed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &quot;Fixed - the test that was failing now passes.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It read the bug, fixed it, and proved the fix without me touching the keyboard. That’s the whole job, done.&lt;/p&gt;
&lt;p&gt;So we’re finished, right? Ship it?&lt;/p&gt;
&lt;p&gt;Not quite.&lt;/p&gt;
&lt;h2 id=&quot;context-is-scarce---and-its-the-harnesses-job-to-keep-it-clean&quot;&gt;Context is scarce - and it’s the harnesses’ job to keep it clean&lt;/h2&gt;
&lt;p&gt;A one-line regex fix is the easy case. The real world looks more like this: before I let the change ship, I want to know if anything &lt;em&gt;depends&lt;/em&gt; on the old buggy behavior. If some other code is feeding &lt;code&gt;&quot;a, b&quot;&lt;/code&gt; to &lt;code&gt;slugify&lt;/code&gt; and expecting &lt;code&gt;a--b&lt;/code&gt; back, my fix just broke it.&lt;/p&gt;
&lt;p&gt;So I ask:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; collapse the repeats in slugify - but first, does any caller depend&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  on the old &quot;a--b&quot; output?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → bash(&quot;grep -rn &apos;slugify(&apos; src/&quot;)    31 hits across 18 files&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → read_file(&quot;src/api/routes.py&quot;)      1/18&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → read_file(&quot;src/cli.py&quot;)             2/18&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ... 16 more reads ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → read_file(&quot;src/feeds.py&quot;)           18/18&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &quot;Reviewed all 18. Honestly? Hard to tell now - probably safe?&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Probably safe.&lt;/em&gt; On a question about whether it’s safe to change shared behavior, “probably” is the one answer I can’t use.&lt;/p&gt;
&lt;p&gt;And it’s not that the model got dumber. Look at what we did to it. Every one of those eighteen files got read into the same conversation, and that conversation is the only thing the model sees on each turn. By the time it reached &lt;code&gt;feeds.py&lt;/code&gt;, the window looked like this:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;parent-context-token-breakdown.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Thousands of tokens for file contents, and somewhere in the middle of it the actual question - &lt;em&gt;does anyone rely on the double hyphen?&lt;/em&gt; - got buried under a landslide of imports and helper functions it didn’t need. The thing it was supposed to be reasoning about was a needle in a haystack the model built for itself.&lt;/p&gt;
&lt;p&gt;This is the trap with one big context window: every tool result piles into the same place, relevant or not, and the signal-to-noise ratio quietly tanks. More reading made it &lt;em&gt;worse&lt;/em&gt;, not better.&lt;/p&gt;
&lt;p&gt;What I actually wanted was for someone to go read all eighteen files, think hard, and come back with one sentence - &lt;em&gt;“checked them all, nobody depends on it, you’re clear”&lt;/em&gt; - without dumping the raw files on my desk. I wanted to delegate.&lt;/p&gt;
&lt;h3 id=&quot;step-6-subagents&quot;&gt;Step 6: Subagents!&lt;/h3&gt;
&lt;p&gt;Which is exactly what a sub-agent is. It’s the same loop we already wrote, running in a fresh, empty context, reachable as just another tool:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; run_subagent&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;task&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    messages &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;task&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)]&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;                       # a brand-new context&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    while&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;                                    # the same loop. literally the same loop.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        r &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;SUBAGENT_SYS&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;tools&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;assistant&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;stop_reason &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; summary_of&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;           # hand back the conclusion, not the mess&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; b &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; tool_uses&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;            messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;dispatch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There’s no new machinery here. It’s &lt;code&gt;model&lt;/code&gt; in a &lt;code&gt;while&lt;/code&gt; loop with its own message list - the agent we built, called from inside the agent we built. The parent reaches it like any other tool:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;report &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; run_subagent&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    task&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Find every caller of slugify; flag any that rely on the old &apos;a--b&apos; output.&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    tools&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; grep&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; glob&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sub-agent burns its &lt;em&gt;own&lt;/em&gt; 22k tokens reading those eighteen files - in a context the parent never sees. All that comes back across the wall is the verdict:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;31 callers, all of them feed URL slugs; none depend on the doubled hyphen. Safe.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eighty tokens instead of twenty-two thousand. The parent stays clean, keeps the thread, and gets a real answer instead of a shrug. The exploring happened somewhere else and only the conclusion made the trip back.&lt;/p&gt;
&lt;p&gt;This is the first decision that isn’t about wiring at all. Nothing forced us to spin up a sub-agent - the single-context version &lt;em&gt;ran fine&lt;/em&gt;, it just gave a worse answer. Where context goes, and what’s allowed to pile up next to what, turns out to be one of the biggest levers you’ve got. We’ll come back to that.&lt;/p&gt;
&lt;h2 id=&quot;dont-make-the-user-the-agents-memory&quot;&gt;Don’t make the user the agent’s memory&lt;/h2&gt;
&lt;p&gt;There’s only one thing that still keeps this harness from being real - you still needs to tell it the same stuff, again and again.&lt;/p&gt;
&lt;p&gt;Every. Session. The context resets, the agent shows up fresh and clueless, and the only thing standing between it and a repeat mistake is the user, remembering to brief it again. That’s backwards - humans are the ones with the bad memory. The harness should manage it!&lt;/p&gt;
&lt;p&gt;And guess what? Most harnesses do.&lt;/p&gt;
&lt;h3 id=&quot;step-7-agent-rules&quot;&gt;Step 7: Agent Rules&lt;/h3&gt;
&lt;p&gt;Some knowledge is &lt;em&gt;always&lt;/em&gt; relevant - the conventions, the gotchas, the “here’s how this repo works.” You want that in front of the model every single time, no questions asked. We call those &lt;strong&gt;rules&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Rules go in a file the harness reads on startup and staples to the system prompt. By convention it’s &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;#&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; AGENTS.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; String utils live in src/text_utils.py; tests in tests/.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; slugify must collapse repeated separators (&quot;a, b&quot; -&gt; &quot;a-b&quot;).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; We&apos;ve had unicode regressions twice - keep the boundary tests.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Verify with: python -m pytest tests/test_text_utils.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. &lt;code&gt;SYSTEM = BASE_PROMPT + open(&quot;AGENTS.md&quot;).read()&lt;/code&gt;, and now every session starts already knowing the lay of the land. The agent never asks “which file is &lt;code&gt;slugify&lt;/code&gt; in” again, because the answer was sitting in its context before it read your first word.&lt;/p&gt;
&lt;p&gt;This is the same &lt;code&gt;AGENTS.md&lt;/code&gt; (or &lt;code&gt;CLAUDE.md&lt;/code&gt;, or &lt;code&gt;.cursorrules&lt;/code&gt;) you’ve seen in real repos - and now you know exactly what it does. It’s a string that gets concatenated onto the system prompt. That’s the entire mechanism.&lt;/p&gt;
&lt;p&gt;It’s important to note, however, that rules do not actually mean that the agent can’t do what they say to avoid doing - they’re the same as regular prompts. The agent can choose to ignore them - which is why you shouldn’t rely on them for guardrails or for keeping the agent from causing harm.&lt;/p&gt;
&lt;h3 id=&quot;skills-expertise-written-once-loaded-on-demand&quot;&gt;Skills: expertise, written once, loaded on demand&lt;/h3&gt;
&lt;p&gt;The other type of knowledge only matters &lt;em&gt;sometimes&lt;/em&gt; - the step-by-step for a specific job you do occasionally. Loading all of it on every turn would just be more haystack. You want it to show up only when the task calls for it. We call those &lt;strong&gt;skills&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For example - “How to safely fix a bug in &lt;code&gt;text_utils.py&lt;/code&gt;” is a real procedure - reproduce with a failing test, make the smallest edit, run the unicode boundary suite, update the changelog - but it’s only relevant when the user is &lt;em&gt;actually&lt;/em&gt; fixing a &lt;code&gt;text_utils&lt;/code&gt; bug. Bolting all of that onto the system prompt for every task, including the ones that never touch text utils, is just noise.&lt;/p&gt;
&lt;p&gt;So a skill lives on disk as its own file, and stays there until it’s needed:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;#&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; skills/fix-text-utils/SKILL.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;name: fix-text-utils&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;description: Fix a bug in src/text_utils.py safely.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;1.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Reproduce: add a failing test for the bug.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;2.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Fix the smallest thing - prefer an anchored edit.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;3.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Run the unicode boundary suite (accents, CJK, emoji).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;4.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Update CHANGELOG.md.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, how does it get loaded only when relevant? You might reach for some clever intent-matching router. You don’t need one. You already have a mechanism that picks the right thing at the right moment based on the task - it’s the model, choosing a tool.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;load_skill&lt;/code&gt; is just another tool. The trick is what goes in its description: a tiny index of every skill and one line on when to use it.&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;load_skill &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; Tool&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;load_skill&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    description&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Load a skill&apos;s full instructions. Available skills:&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;                &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;  fix-text-utils - fix a bug in text_utils.py&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;                &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;  cut-release    - bump version, tag, changelog&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    handler&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=lambda&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; open&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&quot;skills/&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;/SKILL.md&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;read&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;TOOLS &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;read_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; edit_file&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; bash&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; run_subagent&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; load_skill&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model sees the menu - just the names and descriptions, cheap. When I say “fix the slugify bug,” it reads &lt;code&gt;fix-text-utils - fix a bug in text_utils.py&lt;/code&gt;, decides that’s the one, and asks for it like any other tool:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;load_skill&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;fix-text-utils&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dispatch&lt;/code&gt; runs the handler, which is one line - open the file, return its contents - and the full procedure lands in context exactly when it’s useful and never when it isn’t. The expensive part (the whole checklist) only gets paid for on the turns that need it.&lt;/p&gt;
&lt;p&gt;And now the agent runs the unicode suite every time it touches that file. Not because it is smart and understands that this is the right way to do it - no. It’s because the knowledge lives in the harness, and the harness put it in front of the model at the right moment.&lt;/p&gt;
&lt;h2 id=&quot;the-loop-is-small-the-decisions-are-not&quot;&gt;The loop is small. The decisions are not.&lt;/h2&gt;
&lt;p&gt;Let’s step back and look at what we built. Stripped to its bones, the whole thing fits on a napkin:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;    messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    while&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        messages &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; compact&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; BUDGET&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; model&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; system&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;SYSTEM&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tools&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;TOOLS&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;        messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;assistant&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;stop_reason &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;tool_use&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;            print&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;text_from&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;            break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; block &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; tool_uses&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;            messages&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;append&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;dispatch&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;block&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; block&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;That’s a fully working coding agent.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A list, two loops, one HTTP call, one dispatch. There is genuinely nothing else. So if the loop is this small, where does all the difference between a good agent and a useless one actually live? Not in the loop&lt;/p&gt;
&lt;p&gt;It lives in a handful of decisions, and every one of them maps to a line you just read:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What does it SEE?&lt;/strong&gt; → &lt;code&gt;model(messages, system=SYSTEM)&lt;/code&gt;. The system prompt and everything you choose to put in the messages. Get this wrong and the model is flying blind or buried in noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What can it DO?&lt;/strong&gt; → &lt;code&gt;TOOLS = [read, edit, bash]&lt;/code&gt;. Three sharp tools or thirty dull ones. The verbs you hand it are the ceiling on what it can accomplish.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What’s its REACH?&lt;/strong&gt; → &lt;code&gt;TOOLS += [run_subagent, *skills]&lt;/code&gt;. Can it delegate, can it pull in a procedure, can it spin up help. How far past a single conversation it can stretch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What’s REMEMBERED?&lt;/strong&gt; → &lt;code&gt;compact(messages, BUDGET)&lt;/code&gt;. What stays in the window, what gets summarized away, what’s lost. Memory is a budget you spend on purpose.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What’s ALLOWED to run?&lt;/strong&gt; → &lt;code&gt;dispatch(name, args)&lt;/code&gt;. The gate between “the model asked” and “the machine did.” Allow, refuse, rewrite.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What comes BACK?&lt;/strong&gt; → &lt;code&gt;response.content&lt;/code&gt;. The shape of every tool result. A raw file dump or a tight window. Garbage in here is garbage the model has to reason through.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these is in the model. All of them are choices someone made in the harness. That’s the punchline of the whole build: when people say one coding agent is “smarter” than another running the same underlying model, this is what they’re feeling. Not a better brain - better answers to these six questions.&lt;/p&gt;
&lt;p&gt;Two of them carry more weight than the rest, and they’re worth naming. &lt;strong&gt;Tool design&lt;/strong&gt; - what the model can do and what shape the results come back in - and &lt;strong&gt;context engineering&lt;/strong&gt; - what’s in the window and what isn’t. Most of the gap between an agent that nails the task and one that shrugs “probably safe?” comes down to those two. The rest is plumbing.&lt;/p&gt;
&lt;h2 id=&quot;from-theory-to-practice-the-harness-we-actually-run&quot;&gt;From theory to practice: the harness we actually run&lt;/h2&gt;
&lt;p&gt;That whole build was a toy. Forty lines, a slug bug, a story to hang the concepts on. But the reason I care about any of this is I actually got to build a real one for our GTM team at &lt;a href=&quot;https://orb.security&quot;&gt;Orb&lt;/a&gt;, which is how I got to dive into Harness Engineering at the first place.&lt;/p&gt;
&lt;p&gt;Some context. From day one, our goal at Orb was to build an AI-first culture - which meant that we wanted everyone at the company, not just R&amp;#x26;D, to be using AI the way engineers already do - not as a novelty, as the default way work gets done.&lt;/p&gt;
&lt;p&gt;The problem is that an account executive is not going to configure an MCP server or paste an API token into a config file. The tools they live in - Salesforce, Linear, Slack, Notion, Granola - each have their own auth system, and stringing them together is a wall most people bounce off before they get any value. Handing them Claude Code and a setup guide was never going to work.&lt;/p&gt;
&lt;p&gt;So we built our own harness for them. It’s called &lt;code&gt;gtm-os&lt;/code&gt;, it’s built on &lt;a href=&quot;https://pi.dev&quot;&gt;pi.dev&lt;/a&gt; and it’s the same model everyone else uses - we just wrapped it for how this team actually works. Three choices made the difference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No MCP.&lt;/strong&gt; Every integration is a native tool the harness calls directly - using the platform’s native APIs. While this required more work for us up front, it allowed us to make our decisions on how we want the tools to look like and how the authentication story looks like.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Since we own the auth flow, we could use our SSO solution to login into everything!&lt;/strong&gt; Which means that the AE logs in once through the browser with our company SSO - and everything else is auto-magically logged on, with nothing to install or configure. The harness holds that session and reuses the token for every native tool. Salesforce, Linear, Slack, Notion - they all authenticate silently behind the one login. The setup wall is gone because there’s no setup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It’s mission-specific.&lt;/strong&gt; This isn’t a general-purpose agent pretending it can do anything - it has one job, so we baked the flows straight in. On launch it caches the deals the AE owns and watches every prompt for a company, contact, or competitor we track. When it spots one, it injects that context &lt;em&gt;before the model even runs&lt;/em&gt; - so the agent walks in already briefed instead of spending its first three tool calls figuring out who you’re talking about. That’s the “what does it SEE” lever, cranked all the way up.&lt;/p&gt;
&lt;p&gt;Here’s what that buys, concretely. Same request, two harnesses.&lt;/p&gt;
&lt;p&gt;A generic agent, pointed at the same tools over MCP:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;acme-call-prep-failing.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Six round trips, ~18k tokens, about two minutes, and it dead-ends on an auth prompt the AE can’t clear. So they close the tab and do it by hand. That’s the setup wall winning.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gtm-os&lt;/code&gt;, same prompt:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;acme-call-prep-succeeding.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;One tool call, ~3k tokens, about six seconds, done. Nothing about the model changed between those two runs. It’s the same brain. Every bit of that difference - the instant recognition, the silent auth, the answer instead of the shrug - lives in the harness.&lt;/p&gt;
&lt;h2 id=&quot;theres-nothing-stopping-you&quot;&gt;There’s nothing stopping you&lt;/h2&gt;
&lt;p&gt;Go back to that conversation with my colleagues - the smart ones, stuck on “maybe it just sees the whole repo?” The gap was never that harnesses are hard. It’s that nobody had shown them the inside, so the whole thing stayed filed under magic.&lt;/p&gt;
&lt;p&gt;There’s no magic. You watched the entire trick: a list, two loops, one HTTP call, and a function that runs what the model asks for. Everything that makes an agent feel smart - the memory, the tools, the sub-agents, the skills, the sandbox - is plain code wrapped around a model that, left alone, can only emit text. The model is the horse. The harness is the reins. And the reins are the part you can actually build.&lt;/p&gt;
&lt;p&gt;That’s the bit I most want you to walk away with. The same model sits behind Cursor and Claude Code and the thing we run at Orb. They feel different because someone made different choices about what it sees, what it can do, and what comes back - not because anyone had a better brain to work with. Those choices were the whole job.&lt;/p&gt;
&lt;p&gt;They’re also choices you’re completely capable of making.&lt;/p&gt;
&lt;h3 id=&quot;go-deeper&quot;&gt;Go deeper&lt;/h3&gt;
&lt;p&gt;If you want to push past the toy version, here’s where I’d send you. Roughly in the order I’d read them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/context-windows&quot;&gt;&lt;strong&gt;Anthropic - Context Windows&lt;/strong&gt;&lt;/a&gt; - the clearest diagrams I’ve seen of what’s actually piling up in the window turn over turn. The pictures alone are worth it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.claude.com/cookbook/tool-use-context-engineering-context-engineering-tools&quot;&gt;&lt;strong&gt;Anthropic - Context Engineering&lt;/strong&gt;&lt;/a&gt; - the deep version of the lever I called the most important, with a real worked agent (&lt;code&gt;run_research_session&lt;/code&gt;) you can read top to bottom.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.langchain.com/blog/the-anatomy-of-an-agent-harness&quot;&gt;&lt;strong&gt;LangChain - Anatomy of an Agent Harness&lt;/strong&gt;&lt;/a&gt; - the same teardown I did here, from a different angle. Good for triangulating the concepts.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/articles/harness-engineering.html&quot;&gt;&lt;strong&gt;Martin Fowler - Harness Engineering&lt;/strong&gt;&lt;/a&gt; - less of a build guide, but the graphics are great and it takes the “harness as its own discipline” framing seriously.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/jonathans-musings/inside-the-agent-harness-how-codex-and-claude-code-actually-work-63593e26c176&quot;&gt;&lt;strong&gt;Inside the Agent Harness&lt;/strong&gt;&lt;/a&gt; - how Codex and Claude Code actually work under the hood, for when you want the real ones instead of my forty-line one.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shareAI-lab/learn-claude-code/tree/main&quot;&gt;&lt;strong&gt;&lt;code&gt;learn-claude-code&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - a full reference implementation of a harness, if you’d rather read working code than prose. There’s a &lt;a href=&quot;https://learn.shareai.run/en/s01/&quot;&gt;web version&lt;/a&gt; too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the two API docs everything above is built on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/working-with-messages&quot;&gt;&lt;strong&gt;Messages API&lt;/strong&gt;&lt;/a&gt; - the &lt;code&gt;model()&lt;/code&gt; function from the very start, documented properly.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent&quot;&gt;&lt;strong&gt;Tool Use&lt;/strong&gt;&lt;/a&gt; - the contract, the &lt;code&gt;tool_use&lt;/code&gt; / &lt;code&gt;tool_result&lt;/code&gt; round trip, all of it.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.H9MXt591_Z2deeOt.jpeg" medium="image"/><category>Technical</category><category>ai</category><category>harness engineering</category><category>harness</category><category>agents</category></item><item><title>Syncing skills, rules and subagents across repos with amgr</title><link>https://posts.oztamir.com/amgr-syncing-agent-configs-across-repos/</link><guid isPermaLink="true">https://posts.oztamir.com/amgr-syncing-agent-configs-across-repos/</guid><description>I wrote a tool that lets you define your agent setup once - and deploy it everywhere.</description><pubDate>Wed, 04 Feb 2026 12:58:20 GMT</pubDate><content:encoded>&lt;p&gt;If you’ve spent any real time with agentic tools, you’ve felt this: rules, skills, and configs scattered across half a dozen projects with no real linkage. You spin up a new repo and immediately face the same boring questions: where’s my base rule set, what’s the right boilerplate, which tools need which files?&lt;/p&gt;
&lt;p&gt;Even with the ecosystem moving in the right direction, the core problem doesn’t go away. Tools like Vercel’s &lt;a href=&quot;https://skills.sh&quot;&gt;skills.sh&lt;/a&gt; help with &lt;em&gt;what&lt;/em&gt; an agent can do, but not &lt;em&gt;how&lt;/em&gt; you bootstrap a new project, or how you keep different kinds of rules in sync across personal, work, writing, and side projects.&lt;/p&gt;
&lt;p&gt;Last week I ran into it again. I was setting up another repo and caught myself copying the same Cursor rules and docs guidelines I’ve already pasted into three or four other places. It wasn’t hard - just dumb. And it never ends.&lt;/p&gt;
&lt;p&gt;That’s when the question finally landed:&lt;br&gt;
&lt;strong&gt;why am I rebuilding the same agent setup every time I start a new project?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I wanted one source of truth: write once, use everywhere. I couldn’t find a sane way to do it - so I built one.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-one-brain-many-contexts&quot;&gt;The Problem: One Brain, Many Contexts&lt;/h2&gt;
&lt;p&gt;The mess isn’t just file formats. It’s that I need &lt;em&gt;different&lt;/em&gt; agent setups for &lt;em&gt;different&lt;/em&gt; kinds of work, and I’m doing that across a pile of repos.&lt;/p&gt;
&lt;p&gt;Backend repo: heavy coding rules, test commands, debugging skills. Frontend repo: UI patterns, different MCP servers. Writing repo: tone and structure, zero coding noise.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;can&lt;/em&gt; build these setups manually, but it doesn’t scale. If I tweak my base rules, I have to remember which repos should inherit it and which ones shouldn’t. If I add a new “writing voice,” I have to thread it through the right places without leaking into code projects.&lt;/p&gt;
&lt;p&gt;So the real pain is this: &lt;strong&gt;I want one source of truth, but I need selective deployment.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That’s the hole &lt;a href=&quot;https://github.com/OzTamir/amgr&quot;&gt;amgr&lt;/a&gt; (agents manager) is trying to fill.&lt;/p&gt;
&lt;h2 id=&quot;the-idea-one-rules-repo-selective-sync&quot;&gt;The Idea: One Rules Repo, Selective Sync&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/OzTamir/amgr&quot;&gt;amgr&lt;/a&gt;’s mental model is simple: keep a single “rules repo” that holds your agent brain, then sync only the parts each project actually needs.&lt;/p&gt;
&lt;p&gt;In practice, that looks like a repo with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;shared/&lt;/strong&gt; - rules that should apply everywhere&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;development/&lt;/strong&gt; - a nested profile, with sub‑profiles like &lt;code&gt;frontend/&lt;/code&gt; and &lt;code&gt;backend/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;writing/&lt;/strong&gt; - a flat profile for tone, templates, and docs&lt;/li&gt;
&lt;li&gt;(and any other profile you might want)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then each project gets a tiny &lt;code&gt;.amgr/config.json&lt;/code&gt; that says: which tools, which features, and which profiles to pull in.&lt;/p&gt;
&lt;p&gt;You write once. You sync everywhere. And the right rules land in the right repos without manual glue.&lt;/p&gt;
&lt;h2 id=&quot;examples-one-repo-selective-sync&quot;&gt;Examples: one repo, selective sync&lt;/h2&gt;
&lt;p&gt;So what does that look like in practice? Here’s the short version.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/OzTamir/amgr&quot;&gt;amgr&lt;/a&gt; uses a &lt;strong&gt;rules repo&lt;/strong&gt;: one repository where your agent configs live (rules, skills, subagents, commands). You don’t copy files between projects anymore. You sync slices.&lt;/p&gt;
&lt;p&gt;Create one like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;mkdir&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; ~/Code/my-agents&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; cd&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; ~/Code/my-agents&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;amgr&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; repo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; init&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; --name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;my-agents&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I split it into profiles (think: contexts):&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;amgr&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; repo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; development&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; --description&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Coding and debugging&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;amgr&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; repo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; writing&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; --description&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Documentation and content&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the repo has a home for shared rules, and separate buckets for dev vs writing. Mine ends up looking like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;~/Code/my-agents/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── shared/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   └── rules/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│       └── tone.md            # applies everywhere&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── development/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   ├── _shared/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   │   └── rules/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   │       └── testing.md     # shared across dev sub‑profiles&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   ├── backend/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   │   └── agents/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   │       └── db-tuner.md    # a subagent for database work&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│   └── frontend/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│       └── skills/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│           └── ui-checker/    # a skill for UI reviews&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│               └── SKILL.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── writing/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    └── rules/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        └── voice.md           # writing voice + style&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the fun part: each repo only pulls what it needs.&lt;/p&gt;
&lt;h3 id=&quot;example-1-backend-repo-devbackend&quot;&gt;Example 1: backend repo (dev:backend)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.amgr/config.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;targets&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;claudecode&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;cursor&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;profiles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;shared&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;development:backend&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;amgr sync&lt;/code&gt; brings in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared/rules/tone.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;development/_shared/rules/testing.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;development/backend/agents/db-tuner.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No frontend UI skills. No writing voice.&lt;/p&gt;
&lt;h3 id=&quot;example-2-writing-repo-writing&quot;&gt;Example 2: writing repo (writing)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.amgr/config.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;targets&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;claudecode&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;profiles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;shared&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;writing&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared/rules/tone.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writing/rules/voice.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No coding rules. No dev subagents. Clean writing context only.&lt;/p&gt;
&lt;h3 id=&quot;example-3-frontend-repo-devfrontend&quot;&gt;Example 3: frontend repo (dev:frontend)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.amgr/config.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;targets&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;opencode&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;cursor&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;profiles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;shared&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;development:frontend&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get the shared rules, the dev‑shared testing rules, and the &lt;code&gt;ui-checker&lt;/code&gt; skill - but none of the backend‑only subagents.&lt;/p&gt;
&lt;p&gt;That’s the trick. One repo. Many profiles. Each project gets the right slice.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-and-why-now&quot;&gt;Why this matters (and why now)&lt;/h2&gt;
&lt;p&gt;The ecosystem &lt;em&gt;is&lt;/em&gt; moving. As I mentioned, Vercel recently shipped the &lt;a href=&quot;https://vercel.com/changelog/introducing-skills-the-open-agent-skills-ecosystem&quot;&gt;skills CLI and skills.sh directory&lt;/a&gt;, and that’s a big deal. It makes skills shareable and discoverable across tools.&lt;/p&gt;
&lt;p&gt;But skills solve only one part of the problem: the “what can my agent do?” question. The daily pain is still “how do I spin up a new project with the right brain &lt;em&gt;and&lt;/em&gt; keep it in sync?” That’s where a rules repo + selective sync matters.&lt;/p&gt;
&lt;p&gt;If this stuff becomes native across tools, &lt;a href=&quot;https://github.com/OzTamir/amgr&quot;&gt;amgr&lt;/a&gt; should disappear. Until then, I want one source of truth and a clean way to slice it per project.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.v2UocKBR_Z1l4k4H.jpeg" medium="image"/><category>ai</category><category>open-source</category><category>agents</category></item><item><title>Let&apos;s create a Wolt Extension for Raycast</title><link>https://posts.oztamir.com/lets-create-a-wolt-extension-for-raycast/</link><guid isPermaLink="true">https://posts.oztamir.com/lets-create-a-wolt-extension-for-raycast/</guid><description>Building a Raycast extension by reverse-engineering Wolt’s private API</description><pubDate>Sat, 20 Dec 2025 11:31:01 GMT</pubDate><content:encoded>&lt;p&gt;Over the past week, I was introduced to a tool called Raycast. Raycast is a macOS launcher that turns your keyboard into a control center for… pretty much everything.&lt;/p&gt;
&lt;p&gt;Naturally, once I got comfortable with it, I started mapping my daily services into Raycast. Linear? Covered. Home Assistant? Of course. GitHub? Naturally.&lt;/p&gt;
&lt;p&gt;This became sort of a game - looking for new extensions to try, as each one meant a new functionality incorporated into this tool I began to use daily.&lt;/p&gt;
&lt;p&gt;Then I started thinking deeper - what tools don’t have an extension, but an extension for them might be useful?&lt;/p&gt;
&lt;p&gt;I immediately knew the answer - I wanted a Wolt extension! And since none existed, the only solution was to write it out myself.&lt;/p&gt;
&lt;h2 id=&quot;getting-wolt-data&quot;&gt;Getting Wolt Data&lt;/h2&gt;
&lt;p&gt;The first order of business, even before I opened the docs for Raycast, was getting the relevant data from Wolt.&lt;/p&gt;
&lt;p&gt;This is not my first time playing around with the Wolt API - I had &lt;a href=&quot;/wolt-watcher/&quot;&gt;previously wrote&lt;/a&gt; a Telegram bot that interacted with the API, and even made it into a private app a few months later.&lt;/p&gt;
&lt;p&gt;But that was almost 4 years ago - I had no idea if the same API is still in use, and since Wolt does not provide a consumer facing API docs, I had no way of knowing if it changed.&lt;/p&gt;
&lt;p&gt;Luckily for me, I didn’t had to put in the hard work myself - Upon a short google search, I found a repo called &lt;a href=&quot;https://github.com/ilyafeldman/pywolt&quot;&gt;PyWolt&lt;/a&gt; which had already implemented a lot of the required functionality. I played with the examples from the README, and was happy to learn that it works perfectly.&lt;/p&gt;
&lt;p&gt;The only problem? Raycast extensions are written in Typescript, not Python, meaning that I couldn’t just use the library as it is - I needed to rewrite it in Typescript.&lt;/p&gt;
&lt;p&gt;Good thing we live in an AI age.&lt;/p&gt;
&lt;h2 id=&quot;rewriting-a-python-sdk-in-typescript-with-codex&quot;&gt;Rewriting a Python SDK in Typescript with Codex&lt;/h2&gt;
&lt;p&gt;As it just happened to be, it was only a few days ago when I read &lt;a href=&quot;https://github.com/ilyafeldman/pywolt&quot;&gt;this article&lt;/a&gt;by Simon Willison about using Codex, OpenAI’s agentic coding tool, to port a Python library called JustHTML (itself &lt;a href=&quot;https://friendlybit.com/python/writing-justhtml-with-coding-agents/&quot;&gt;written by coding agents&lt;/a&gt;) into Typescript.&lt;/p&gt;
&lt;p&gt;This seemed to me like an interesting concept to play with, and an interesting detour from my main quest - but a one worth taking. My plan was simple, and it involved two steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I will first generate a specs docs based on the existing Python implementation.&lt;/li&gt;
&lt;li&gt;I will then use this document to create a Typescript library.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As a side note, this is quite different from the approach used by Simon, who had just gave Codex access to the full library code. The reason I went with a different path is that unlike JustHTML, here I cared less about the actual library - it’s just a wrapper around the Wolt API, and I cared more about the API itself than about the library.&lt;/p&gt;
&lt;h3 id=&quot;creating-a-spec-document&quot;&gt;Creating a Spec document&lt;/h3&gt;
&lt;p&gt;To kickoff the first step, I cloned the PyWolt repo and started Codex in it. I gave it a short prompt - &lt;code&gt;read this codebase and produce a markdown documentation of the Wolt API&lt;/code&gt; and sent it on its way.&lt;/p&gt;
&lt;p&gt;Along the process, I also asked it to add &lt;code&gt;cURL&lt;/code&gt; examples and to run these examples by itself beforehand to ensure that they work and that the document is accurate.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;claude-api-curl-verification.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The completed document is available as a gist &lt;a href=&quot;https://gist.github.com/OzTamir/7ed2b49b8628d88c086d65b0d1730c36&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;creating-a-library&quot;&gt;Creating a library&lt;/h3&gt;
&lt;p&gt;For the actual implementation of the spec, I choose to again deviate from Simon’s article and used Cursor with &lt;code&gt;Composer 1&lt;/code&gt; as the model. I had a good experience with it, its blazing fast, and since this project does not require a lot of heavy thinking, I figured I could get away with not using the SOTA thinking models in this project.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;wolt-typescript-library-summary.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Much to my delight, this proved to be a good decision - as Composer 1 was able to effectively one-shot this task. The only missing item was tests, but after pointing it out Composer stepped up and finished the job.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;vitest-setup-writing-tests.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;After ensuring that everything work, I used &lt;code&gt;npm publish&lt;/code&gt; to push the library to &lt;code&gt;npm&lt;/code&gt;, where it is now &lt;a href=&quot;https://www.npmjs.com/package/wolt-api&quot;&gt;available to install&lt;/a&gt;. The source code is available &lt;a href=&quot;https://github.com/OzTamir/wolt-api&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;creating-a-raycast-extension&quot;&gt;Creating a Raycast Extension&lt;/h2&gt;
&lt;p&gt;Once I had the library ready to go, it was time to get back to the main storyline - and start working on the Raycast extension.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developers.raycast.com/basics/create-your-first-extension&quot;&gt;I read the documentation&lt;/a&gt; and learned that in order to create a Raycast extension, one should - how convenient - use Raycast! This is done through a command called “Create New Extension”, which opens a configuration dialog.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;raycast-create-extension-form.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;An interesting part of this process is that you can configure the commands and tools that will be made available through your extension through this creation command. While I will be adding multiple commands and tools, I elected to start with a simple one - venue search - in order to get acquainted with the SDK before diving into deep water.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;raycast-search-venues-command.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;I let the wizard run, and was greeted with a new repository containing a template of a command that searches &lt;code&gt;npm&lt;/code&gt; - exactly the same experience I wanted to replicate with my extension!&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;search-venues-code-raycast-ui.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;implementing-commands-and-tools&quot;&gt;Implementing Commands and Tools&lt;/h2&gt;
&lt;p&gt;From that moment onwards, most of what I did was prompting an agent to wrap the library we wrote at the beginning with commands and tools.&lt;/p&gt;
&lt;p&gt;The first tool I implemented was the venue search - and it was a delight to create, as the Raycast SDK is very easy to use. It was simply about getting a response from the API and rendering it into a list view.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;wolt-api-search-venues-code.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Once I added the basic commands, I started adding more and more functionality - I added a way to pick a city (using the Raycast preferences APIs), I allowed users to view the menu of a venue through the extension, and more.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;wolt-venue-menu-action-code.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This was a really interesting experience because as I went along I found some issues in my library code. Luckily for me, I could simply use Cursor’s browser tab to debug any API calls that did not work, which saved me a ton of time and effort.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;wolt-menu-browser-reverse-engineering.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;publishing-the-extension&quot;&gt;Publishing the Extension&lt;/h2&gt;
&lt;p&gt;The last part of this process is to &lt;a href=&quot;https://developers.raycast.com/basics/publish-an-extension&quot;&gt;publish the extension&lt;/a&gt; to make it available in the Raycast Store.&lt;/p&gt;
&lt;p&gt;Like the other steps in this process, this was also fairly easy - all it took was to run a pre-configured &lt;code&gt;npm&lt;/code&gt; script that opens a pull request at the &lt;a href=&quot;https://github.com/raycast/extensions&quot;&gt;Raycast Extensions repository&lt;/a&gt; on Github.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;npm-run-publish-raycast-extension.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;While this will (hopefully) be merge into the bigger repository soon, the code is also available on &lt;a href=&quot;https://github.com/OzTamir/wolt-raycast-extension&quot;&gt;my personal repository&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;This was a fun project, and I’m very happy with the result - while not the most practical thing in the world, this was a good experience for trying out Raycasts extensions.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;raycast-wolt-pasta-search.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Additionally, I think that I might make a lot of use out of this API wrapper - I already have plans to play around with a Wolt MCP for automatic food ordering.&lt;/p&gt;
&lt;p&gt;Stay tuned 👀&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.CUohhrCH_Z9bj2M.jpeg" medium="image"/><category>Raycast</category><category>Projects</category><category>Technical</category></item><item><title>From WhatsApp to Todoist: an n8n + Whatsmeow automation</title><link>https://posts.oztamir.com/from-whatsapp-to-todoist-an-n8n-whatsmeow-automation/</link><guid isPermaLink="true">https://posts.oztamir.com/from-whatsapp-to-todoist-an-n8n-whatsmeow-automation/</guid><description>Using whatsmeow, n8n, and a Gemini-based agent, my WhatsApp commitments are now synced to Todoist.</description><pubDate>Tue, 14 Oct 2025 13:58:26 GMT</pubDate><content:encoded>&lt;p&gt;I make a lot of promises on WhatsApp.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Yeah, I’ll send that later.”&lt;br&gt;
“Sure, I’ll ask him.”&lt;br&gt;
“Remind me to book it tomorrow.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But more often than not, I never do.&lt;/p&gt;
&lt;p&gt;It’s not that I don’t want to do these things. It’s just that unless I stop everything I’m doing right there, open Todoist, and add the task manually, there’s a solid 50/50 chance I’ll forget about it entirely.&lt;/p&gt;
&lt;p&gt;After this had happened one time too many, I figured that it’s time to do something about it. After all, this is 2025 - the age of AI! It’s simply unacceptable that so many of the tasks I commit depend on a me remembering to file them. There has to be a better way.&lt;/p&gt;
&lt;h2 id=&quot;the-plan-monitoring-messages-with-ai&quot;&gt;The plan: Monitoring messages with AI&lt;/h2&gt;
&lt;p&gt;Once I accepted that I clearly can’t be trusted to remember my own commitments, I figured I might as well outsource the job.&lt;/p&gt;
&lt;p&gt;The goal was simple: I wanted something that could read my WhatsApp messages, figure out when I promised to do something, and make sure it ended up in Todoist - without me lifting a finger.&lt;/p&gt;
&lt;p&gt;At first, I thought this would be easy. Just plug into WhatsApp somehow, run the messages through an LLM, connect Todoist’s API, done. Right?&lt;/p&gt;
&lt;p&gt;Not quite.&lt;/p&gt;
&lt;p&gt;Turns out, there’s no “nice” way to monitor your own WhatsApp messages. The official APIs are meant for businesses, so if I wanted to make this work, I’d need to start with step one: getting access to my own WhatsApp data.&lt;/p&gt;
&lt;h2 id=&quot;what-about-the-whatsapp-mcp-server&quot;&gt;What about the WhatsApp MCP Server?&lt;/h2&gt;
&lt;p&gt;If you recall, in a &lt;a href=&quot;/i-now-use-ai-agents-to-text-you-back/&quot;&gt;previous post&lt;/a&gt; I used a WhatsApp MCP server to do something similar. So you might ask - why not use it again?&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;whatsapp-mcp-server-readme.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Well, unlike in the previous post, this project was meant to run in a cloud hosted VPS. Now, while I trust in my ability to secure servers, this means that the risk is higher - and I wouldn’t want my entire WhatsApp history sitting in a VPS.&lt;/p&gt;
&lt;p&gt;I also didn’t need all the bells and whistles that come with MCPs. I wasn’t trying to build a chatty AI that replies to people. I just needed a way to read messages.&lt;/p&gt;
&lt;h2 id=&quot;building-a-whatsapp-listener&quot;&gt;Building A WhatsApp Listener&lt;/h2&gt;
&lt;p&gt;So, what did I do?&lt;/p&gt;
&lt;p&gt;I read through the &lt;a href=&quot;https://github.com/lharries/whatsapp-mcp&quot;&gt;MCP Server documentation&lt;/a&gt;, and found out that it is using a Go package called &lt;a href=&quot;https://github.com/tulir/whatsmeow&quot;&gt;whatsmeow&lt;/a&gt; that implements a WhatsApp client using their Web MultiDevice APIs.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;whatsmeow-library-docs.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;So, I took the example code for this package and hacked together a small Dockerized application called &lt;a href=&quot;https://github.com/OzTamir/whatsapp-aggragetor/&quot;&gt;&lt;code&gt;whatsapp-aggregator&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This app uses the API to listen in on new messages and store them in a local Sqlite3 database:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;client&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;AddEventHandler&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{})&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;	switch&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; v&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; evt&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.(&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;	case&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;events&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Message&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;		senderType&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;		if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; v&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;Info&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;IsFromMe&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;			senderType&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;sent by me to&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;		log&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;Printf&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Received message &lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;%s&lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt; %s&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; in chat &lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;%s&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;%s&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;			senderType&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; v&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;Info&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;Sender&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; v&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;Info&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;Chat&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; v&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;Message&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;GetConversation&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#616E88&quot;&gt;		// Store message in database&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;		if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; err&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; messageDB&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;InsertMessage&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;v&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; err&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; !=&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;			log&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;Printf&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Failed to store message: &lt;/span&gt;&lt;span style=&quot;color:#EBCB8B&quot;&gt;%v&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; err&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;		}&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;			log&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;Printf&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Message stored successfully&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In addition, to make sure that we store as little data on the server as possible, a Python script is also running alongside the Go package.&lt;/p&gt;
&lt;p&gt;This script runs every few minutes and deletes old messages - ensuring that the server only keeps the last 24-hours worth of messages.&lt;/p&gt;
&lt;h2 id=&quot;gathering-context-for-the-agent&quot;&gt;Gathering Context for the Agent&lt;/h2&gt;
&lt;p&gt;With the messages now available on my server, I needed to deal with the automation part.&lt;/p&gt;
&lt;p&gt;Like in previous cases, I decided that I would use &lt;code&gt;n8n&lt;/code&gt; for the workflow, but quickly ran into an issue - &lt;code&gt;n8n&lt;/code&gt; did not have a native Sqlite integration!&lt;/p&gt;
&lt;p&gt;Luckily, the &lt;code&gt;n8n&lt;/code&gt; community nodes came to the rescue - and I found a simple node that allowed me the query the DB called &lt;a href=&quot;https://github.com/DangerBlack/n8n-node-sqlite3&quot;&gt;&lt;code&gt;n8n-node-sqlite3&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-fetch-messages-subworkflow.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Next order of business was getting existing TODOs out from Todoist. I didn’t want to get duplicates - so if there’s already a task for something I mentioned on WhatsApp, I didn’t want the agent to create another one.&lt;/p&gt;
&lt;p&gt;To get the existing tasks, I initially used the &lt;a href=&quot;https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.todoist/?utm_source=n8n_app&amp;#x26;utm_medium=node_settings_modal-credential_link&amp;#x26;utm_campaign=n8n-nodes-base.todoist&quot;&gt;n8n Todoist node&lt;/a&gt; - but I quickly ran into an issue. When using this node to query existing tasks, it would not return completed tasks - which meant that once I marked a task as done it would be re-added by the agent.&lt;/p&gt;
&lt;p&gt;To go around this issue, I used a custom HTTP node that directly called &lt;a href=&quot;https://developer.todoist.com/api/v1/#tag/Tasks/operation/tasks_completed_by_completion_date_api_v1_tasks_completed_by_completion_date_get&quot;&gt;the API&lt;/a&gt; to get the completed tasks.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;todoist-api-docs.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Once I had both the completed tasks, the open tasks, and the messages history, I had all the context I needed to fire up the agent itself.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-main-workflow-partial.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;creating-a-gemini-agentusing-claude&quot;&gt;Creating a Gemini Agent…using Claude???&lt;/h2&gt;
&lt;p&gt;Now, I knew I wanted to use Gemini as the LLM for this agent because I had good experience with how it handles tasks. However, I knew that writing the prompt for this on my own would be a hard task.&lt;/p&gt;
&lt;p&gt;Instead, I decided to use another LLM! I fired up Claude Sonnet, and fed it instructions on how I want this agent to work.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;ai-agent-system-prompt.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;In addition to my instructions, I gave it this screenshot, from the Anthropic talk &lt;a href=&quot;https://youtu.be/ysPbXH0LpIE&quot;&gt;&lt;code&gt;Prompting 101&lt;/code&gt;&lt;/a&gt; - which I recommend watching for everyone working on agents.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;prompt-structure-diagram.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Basically, this is the structure recommended by Anthropic researchers for how to construct a prompt - including things like context engineering, giving the model concrete examples, and more.&lt;/p&gt;
&lt;p&gt;After some back and forth with Claude, I had a 500-lines long prompt that started like this:&lt;br&gt;
&lt;code&gt;You are an intelligent task management assistant that analyzes WhatsApp conversations and Todoist tasks to identify missing action items. Your goal is to help users capture commitments and action items that haven&apos;t been added to their task list yet.      You should maintain a precise, analytical tone. Be conservative in your analysis - it is better to miss an action item than to create duplicate or unnecessary tasks.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The full prompt is available &lt;a href=&quot;https://gist.github.com/OzTamir/65b05baf7a0f66e0756f6dbd46868c44&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;creating-the-task-and-notifying-about-it&quot;&gt;Creating the task and notifying about it&lt;/h2&gt;
&lt;p&gt;Once the agent had came up with a list of tasks, all that’s left to be done is to add them to Todoist.&lt;/p&gt;
&lt;p&gt;For this I am once again using the Todoist node - and I make sure to add the &lt;code&gt;n8n-generated&lt;/code&gt; label to ensure that I know that these tasks were auto-generated.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-create-todoist-task-node.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Once the task had been created, I added one final node that uses &lt;a href=&quot;https://pushover.net/&quot;&gt;Pushover&lt;/a&gt; to sends out a notification about the new task with a link to the task in Todoist.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-pushover-notify-node.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This is useful to allow me to get feedback that the system is running properly and that new tasks are being added.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;pushover-task-notification.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And that’s it. My “personal assistant” now quietly listens to my WhatsApp chats, figures out what I’ve committed to, and keeps Todoist up to date. This is what the final &lt;code&gt;n8n&lt;/code&gt; workflow looks like:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-full-workflow.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;It’s a small thing, but it echos my favorite type of hacks - ones that blend seamlessly into the background of your life, and help make them better - one message at a time.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.Bjn12GSl_Z1f5J84.jpeg" medium="image"/><category>n8n</category><category>automation</category><category>agents</category><category>ai</category><category>Technical</category></item><item><title>Reverse Engineering APIs with Chrome DevTools MCP</title><link>https://posts.oztamir.com/reverse-engineering-apis-with-chrome-devtools-mcp/</link><guid isPermaLink="true">https://posts.oztamir.com/reverse-engineering-apis-with-chrome-devtools-mcp/</guid><description>Why should I go through network requests when an agent can do it for me?</description><pubDate>Sat, 27 Sep 2025 11:36:47 GMT</pubDate><content:encoded>&lt;p&gt;As all good stories go, this one too starts with a wedding - my wedding.&lt;/p&gt;
&lt;p&gt;As part of the preparations, I found myself working on a small vibe-coded project that required me to get my hands on a bunch of meme templates. We are not talking one or two - I needed every template I could get my hands on.&lt;/p&gt;
&lt;p&gt;Luckily for me, I found an Israeli website loaded with a bunch of templates - thousands of memes, neatly categorized. Exactly what I needed.&lt;/p&gt;
&lt;p&gt;The only problem: there was no easy way to get them out. No export button, no public API, just an endless scroll of templates locked behind the frontend.&lt;/p&gt;
&lt;p&gt;So naturally, I decided I’d have to scrape it.&lt;/p&gt;
&lt;h2 id=&quot;enter-chrome-devtools-mcp&quot;&gt;Enter: Chrome DevTools MCP&lt;/h2&gt;
&lt;p&gt;While I was pondering what is the right approach here, I was scrolling X when I came across a newly released tool.&lt;/p&gt;
&lt;p&gt;The Chrome team &lt;a href=&quot;https://developer.chrome.com/blog/chrome-devtools-mcp&quot;&gt;had just announced&lt;/a&gt; that they are releasing a public beta of their DevTools MCP - way for agents to interact directly with the browser.&lt;/p&gt;
&lt;p&gt;Reading through their announcement, one feature immediately caught my attention: the MCP provided tools for inspecting network requests.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;devtools-mcp-network-tools.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Could this be exactly what I needed? Instead of manually digging through requests in DevTools, could I just point an agent at the site and ask it to figure out the meme website’s API for me?&lt;/p&gt;
&lt;h2 id=&quot;task-failed-successfully&quot;&gt;Task Failed Successfully&lt;/h2&gt;
&lt;p&gt;I set up the new Chrome MCP and connected it to Claude Code, which I instructed to research the website and come up with an API client.&lt;/p&gt;
&lt;p&gt;This is the prompt I was using:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;md&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;You are a QA Engineer tasked with creating a Python script that interacts with an API in the company&apos;s website.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;However, to ensure accurate testing, you were not provided with the API specs.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Instead, I want you to use your Chrome Tools to go to &lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;`[MEME_WEBSITE]`&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; and inspect the network requests until you figure out how to interact with the API.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;You can use other Chrome tools to interact with the website if needed.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Your output should be a Python script that implements an API client and another script that consumes it.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Use ultrathink .&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;(If you’re unfamiliar - &lt;code&gt;ultrathink&lt;/code&gt; &lt;a href=&quot;https://simonwillison.net/2025/Apr/19/claude-code-best-practices/&quot;&gt;is a magic word&lt;/a&gt; that makes Claude work harder)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On paper, it sounded perfect. Claude would load the site, inspect the network requests through Chrome’s MCP, and reverse engineer the API without me having to touch DevTools manually.&lt;/p&gt;
&lt;p&gt;In reality, it failed almost immediately.&lt;/p&gt;
&lt;p&gt;The problem was that the website was sending too many requests, and when the agent used the MCP tool to get information, it would immediately hit it’s token limits.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;token-limit-error.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The experiment had technically worked — the MCP was capturing requests just fine — but there was simply too much noise for the LLM to handle.&lt;/p&gt;
&lt;h2 id=&quot;open-source-to-the-rescue&quot;&gt;Open Source to the Rescue&lt;/h2&gt;
&lt;p&gt;At first I thought that was the end of it. If the MCP was going to drown in every single request, then this whole approach was dead on arrival.&lt;/p&gt;
&lt;p&gt;But then I remembered — this wasn’t a black box product from Google. The DevTools MCP is &lt;a href=&quot;https://github.com/ChromeDevTools/chrome-devtools-mcp&quot;&gt;open source&lt;/a&gt;. Which meant I didn’t have to accept its limitations. I could just… fix them.&lt;/p&gt;
&lt;p&gt;So I cracked it open and started coding.&lt;/p&gt;
&lt;p&gt;The main issue was that the &lt;code&gt;list_network_requests&lt;/code&gt; tool returned everything in one giant dump. To overcome this, I added some parameters to the tool - allowing the calling agent to paginate and filter so that it would only get the requests it cares about, and would be able to process them in manageable chunks.&lt;/p&gt;
&lt;p&gt;Half an hour later, I had a &lt;a href=&quot;https://github.com/ChromeDevTools/chrome-devtools-mcp/pull/145&quot;&gt;pull request open&lt;/a&gt; - and not long after, it was merged.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;pagination-pr-merged.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;For me, this was yet another cool demonstration of how open source tools are the best - if something ain’t working for you, you can simply jump to the trenches and fix it for you and for everyone.&lt;/p&gt;
&lt;h2 id=&quot;one-shotting-api-clients-with-claude&quot;&gt;One-shotting API clients with Claude&lt;/h2&gt;
&lt;p&gt;With my little side-side-quest over, I got back to the original side-quest - getting Claude Code to reverse engineer an API for me.&lt;/p&gt;
&lt;p&gt;This time, thanks to the new pagination option, Claude was able to process the API requests in small chunks - and what can I say?&lt;/p&gt;
&lt;p&gt;IT CRUSHED IT.&lt;/p&gt;
&lt;p&gt;First, it identified the endpoints behind the meme feed:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;agent-network-inspection.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Then, it mapped out how the site organized categories, how the JSON payloads looked, and where the images were actually hosted:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;discovered-api-endpoints.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;And once it had a clear enough picture, it went ahead and started generating the Python API client:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;generated-python-client.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Boom. Nearly 300 lines of clean, working code. A fully reverse-engineered client for an undocumented API, generated in one shot.&lt;/p&gt;
&lt;p&gt;Watching this unfold in real time felt surreal - like pair-programming with an intern who could read Chrome’s network tab faster than humanly possible.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Chrome DevTools MCP isn’t just another developer toy. It’s a genuine superpower for anyone doing web research, scraping, or API reverse engineering.&lt;/p&gt;
&lt;p&gt;The ability to hook an agent directly into Chrome and let it see the same network requests you would in DevTools completely changes the workflow.&lt;/p&gt;
&lt;p&gt;If your work involves poking at undocumented APIs, this tool deserves a spot in your toolbox. It takes what used to be a tedious, manual grind and turns it into a collaborative process with an agent that can actually do the heavy lifting.&lt;/p&gt;
&lt;p&gt;And in my case? Well, all I got left is to make something worthwhile with all these meme templates.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.VspCXjVF_1O1Xdh.jpeg" medium="image"/><category>mcp</category><category>agents</category><category>Programming</category><category>website</category><category>Technical</category></item><item><title>Solving Multi-Calendar Conflicts with n8n</title><link>https://posts.oztamir.com/solving-multi-calendar-conflicts-with-n8n/</link><guid isPermaLink="true">https://posts.oztamir.com/solving-multi-calendar-conflicts-with-n8n/</guid><description>How I defended my WLB with a Google Calendar workflow</description><pubDate>Mon, 02 Jun 2025 15:18:26 GMT</pubDate><content:encoded>&lt;p&gt;Like most folks, my work calendar and my personal ones live in different accounts. And when I schedule something over on my private calendar (one of them, I use four different ones because I’m crazy), my coworkers don’t see any of that - they only see my work calendar.&lt;/p&gt;
&lt;p&gt;So when they’re looking for a free slot, it looks like I’m wide open - even if I’ve already committed to something else. And it’s driving me crazy.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;slack-reschedule-request.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This past week, after enough double-bookings and awkward “hey, can we reschedule?” messages, I got tired of manually fixing things. I wanted my work calendar to at least know when I’m busy, even if it doesn’t need to know why.&lt;/p&gt;
&lt;p&gt;Like most recurring annoyances in my life, this felt like something I should be able to automate.&lt;/p&gt;
&lt;h2 id=&quot;oh-yeah-its-automation-time&quot;&gt;Oh yeah, it’s automation time&lt;/h2&gt;
&lt;p&gt;The idea was simple: take events from my personal calendars and reflect them onto my work calendar as generic “busy” blocks. No details, no descriptions. Just enough to stop coworkers from accidentally double-booking me.&lt;/p&gt;
&lt;p&gt;To make this work, I had a few clear requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local-first&lt;/strong&gt; - I didn’t want to give any third-party service read/write access to both my personal and work calendars.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full control&lt;/strong&gt; - I wanted to decide which events to sync, how they’d be translated, and what metadata (if any) gets shared.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal setup&lt;/strong&gt; - Ideally, something I could get running in under an hour without writing a pile of boilerplate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since I already had a local n8n instance running (left over from another automation &lt;a href=&quot;/i-now-use-ai-agents-to-text-you-back/#setting-up-n8n&quot;&gt;I built around WhatsApp MCPs&lt;/a&gt;), it felt like the obvious choice. It ticked all the boxes — and I jumped straight into it.&lt;/p&gt;
&lt;h2 id=&quot;setting-up-google-calendar-access&quot;&gt;Setting Up Google Calendar Access&lt;/h2&gt;
&lt;p&gt;Before I could do anything useful, I needed to let n8n access my personal calendars.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;google-oauth-permission-screen.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;That meant setting up a Google Cloud project, enabling the Calendar API, configuring the OAuth consent screen, and creating OAuth2 credentials.&lt;/p&gt;
&lt;p&gt;Nothing too surprising, just a series of Google Console forms you have to fight through. In the credentials setup, I selected “Web application” as the type and added n8n’s OAuth2 redirect URI.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;gcloud-oauth-client-list.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Once Google gave me the Client ID and Secret, I dropped them into a new credential in n8n using the built-in Google OAuth2 option.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-calendar-credential-form.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;If you’ve ever done anything with Google APIs, you’ve seen this dance before. If not, the &lt;a href=&quot;https://docs.n8n.io/integrations/builtin/credentials/google/oauth-generic/#enable-apis&quot;&gt;n8n docs&lt;/a&gt; explain it better than I want to.&lt;/p&gt;
&lt;h2 id=&quot;automating-the-sync&quot;&gt;Automating the Sync&lt;/h2&gt;
&lt;p&gt;The workflow I created runs every 6 hours. That’s usually frequent enough to keep things in sync without spamming the API or hammering my work calendar.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-full-sync-workflow.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The first thing it does is fetch events from my personal calendars that were updated in the last 6 hours.&lt;/p&gt;
&lt;p&gt;This includes new events, updates, and cancellations - basically anything that might require an update on the work calendar side.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-get-events-node.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Each calendar (I have a few) gets queried separately using the &lt;code&gt;getAll&lt;/code&gt; operation on the Calendar API.&lt;/p&gt;
&lt;p&gt;I use the &lt;code&gt;updatedMin&lt;/code&gt; parameter to filter only recent changes, which is 6 hours in my case:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{{ $now.minus({ hour: 6 }) }}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After pulling from all of my personal calendars, the events are merged into a single list and processed from there.&lt;/p&gt;
&lt;h2 id=&quot;syncing-events-to-work-calendar&quot;&gt;Syncing Events to Work Calendar&lt;/h2&gt;
&lt;p&gt;Once all updated events are pulled and merged, the workflow goes over each one and decides what to do based on its state.&lt;/p&gt;
&lt;h3 id=&quot;cancelled-events&quot;&gt;Cancelled Events&lt;/h3&gt;
&lt;p&gt;If the event was cancelled, it’s deleted from the work calendar. I check for &lt;code&gt;status === &quot;cancelled&quot;&lt;/code&gt; and route those into a Delete node.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-cancelled-condition-node.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Nothing fancy. If it’s no longer happening, it shouldn’t block time on the work side either.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-delete-event-node.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;new-events&quot;&gt;New Events&lt;/h3&gt;
&lt;p&gt;If the event is new, it gets copied over to the work calendar. The copied version keeps the same start and end time, but I override a few fields to keep things clean and private:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The event is marked as &lt;code&gt;private&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The title is just &lt;code&gt;&quot;Blocked for Personal Event&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Visibility is set to “busy” so it shows up as unavailable to coworkers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I might eventually include the original event title here, but since I don’t know exactly who can see what in our org’s calendar setup, I’m staying on the side of caution for now.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-create-blocker-node.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;updated-events&quot;&gt;Updated Events&lt;/h3&gt;
&lt;p&gt;If the event already exists on the work calendar (based on ID), the creation step throws an error. I catch that and instead run an update, making sure the start and end times are synced.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-update-event-node.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This takes care of reschedules or time changes without having to delete and recreate anything.&lt;/p&gt;
&lt;h1 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h1&gt;
&lt;p&gt;This workflow has been running quietly in the past week, and so far it’s doing exactly what I wanted: keeping my work calendar aware of my personal time, without oversharing or manual juggling.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;blocked-personal-event-result.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;It’s not doing anything groundbreaking (just moving data from A to B with a bit of filtering and formatting) but that’s the kind of automation I like best. Clear input, predictable output, no surprises.&lt;/p&gt;
&lt;p&gt;I might tweak it over time (maybe loosen up the visibility settings, or add color-coding per calendar), but for now, it’s doing the job.&lt;/p&gt;
&lt;p&gt;My calendars don’t fight anymore.&lt;/p&gt;
&lt;p&gt;That’s a win.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.Bm56Nk9h_I1eh7.jpeg" medium="image"/><category>n8n</category><category>automation</category><category>calendar</category></item><item><title>Claude is now my micro-manager</title><link>https://posts.oztamir.com/claude-is-now-my-micro-manager/</link><guid isPermaLink="true">https://posts.oztamir.com/claude-is-now-my-micro-manager/</guid><description>OR: How to hit your OKRs with AI and MCPs</description><pubDate>Wed, 16 Apr 2025 09:01:34 GMT</pubDate><content:encoded>&lt;p&gt;I recently sat down with my manager to lay out the plan for the coming quarter. We framed the conversation around OKRs as a way to get clear on what we actually want to achieve.&lt;/p&gt;
&lt;p&gt;For those unfamiliar, OKR (which stands for Objectives and Key Results) is a goal-setting framework popularized by companies like Google.&lt;/p&gt;
&lt;p&gt;The idea is simple: you set a few high-level objectives that define what success looks like and then attach measurable key results that tell you whether you’re getting there. It’s like a to-do list if your most ambitious self wrote it.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img src=&quot;https://media.licdn.com/dms/image/v2/D4E12AQFyLeSN948Fyw/article-inline_image-shrink_1500_2232/article-inline_image-shrink_1500_2232/0/1682419016770?e=1749686400&amp;#x26;v=beta&amp;#x26;t=D1aPF_3_i6xY8mQ3KbgPkGngsVZorvB788150wS8HT0&quot; alt=&quot;OKR (Objectives and Key Results): Applying the popular business  goal-setting framework to personal goals&quot;&gt;&lt;figcaption&gt;Source: &lt;a href=&quot;https://www.linkedin.com/pulse/okr-objectives-key-results-applying-popular-business-framework/&quot;&gt;Dmytro Yarmak&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;I know some people dislike OKRs, but I, for one, love them. They’re a great way to cut through the noise and focus on what matters. They force you to think strategically, pick a direction, and aim for impact - not just activity.&lt;/p&gt;
&lt;p&gt;But while OKRs are great at helping you zoom out, they don’t always help when you zoom in. They tell you what to achieve but not how to get there. And if you’re anything like me, that’s where things can start to wobble - especially once the quarter is underway and your calendar is full of other people’s priorities.&lt;/p&gt;
&lt;p&gt;So I enlisted some help - I asked an LLM to micro-manage me.&lt;/p&gt;
&lt;h2 id=&quot;the-idea-let-the-llm-do-the-planning&quot;&gt;The idea? Let the LLM do the planning&lt;/h2&gt;
&lt;p&gt;Typically, once the OKRs were set, I’d sit down and try to break each Key Result into subtasks, estimate how long things might take, juggle dependencies, and then still get halfway through the quarter and realize I forgot something important.&lt;/p&gt;
&lt;p&gt;But this time, I wanted to try something different. Instead of doing all the breakdown and planning manually, I asked an LLM to help.&lt;/p&gt;
&lt;p&gt;My thinking was simple: this isn’t a creative task - it’s a reasoning task. It has structure, inputs, and constraints, making it perfect for a model good at structured reasoning.&lt;/p&gt;
&lt;h2 id=&quot;building-the-plan&quot;&gt;Building the plan&lt;/h2&gt;
&lt;p&gt;To get started, I used OpenAI’s reasoning model - O1. The ask was simple: “Turn my OKRs into an actual execution plan: week-by-week, task-by-task.”&lt;/p&gt;
&lt;p&gt;Because I know that I sometimes struggle with context-switching, I told it to assume a “one day = one task” cadence - no juggling multiple things in a single day. Additionally, I asked it to break down anything too big into smaller chunks. I asked it to consider task dependencies and keep the whole timeline in mind to ensure I hit the goals on time.&lt;/p&gt;
&lt;p&gt;This is the prompt I used:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;md&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;These are my OKRs for the coming quarter.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Today is April 16. The OKR target date is July 31.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Help me plan a week-by-week task list - each task should be something that can be done in a day (if something needs more time, break it down into smaller tasks).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;For each task, you should write:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Title&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Description&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Start date&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; DoD (Definition of Done)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Organize the tasks into logical groups - each group should translate into one of the key results. For each group, set a start date and an end date.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Make sure you take into account both the target dates in the OKR and the fact that some of the KRs are dependent on one another.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result wasn’t magic - but it was good. It was good enough that with just a little bit of back-and-forth, I had something that looked like an actual, trackable plan.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;quarterly-okr-plan.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Each KR had a timeline. Each task had a clear outcome. And most importantly, I could look at any given week and know exactly what I needed to do.&lt;/p&gt;
&lt;p&gt;Planning done. Now, it was time to get everything up on the board.&lt;/p&gt;
&lt;h2 id=&quot;getting-the-tasks-into-a-system&quot;&gt;Getting the Tasks Into a System&lt;/h2&gt;
&lt;p&gt;With the plan in place, the next step was getting it into the tools I use.&lt;/p&gt;
&lt;p&gt;My task management app of choice is &lt;a href=&quot;https://todoist.com&quot;&gt;Todoist&lt;/a&gt;. Historically, this is the part where I’d either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spend an hour manually copying tasks in, or&lt;/li&gt;
&lt;li&gt;Spend &lt;em&gt;two&lt;/em&gt; hours writing a one-off script to push them in via API.&lt;br&gt;
(And then not touch it again until next quarter.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;But this is 2025, and we have better tools now - Specifically, &lt;a href=&quot;/i-now-use-ai-agents-to-text-you-back/&quot;&gt;MCPs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are a few Todoist MCP servers available. I used &lt;a href=&quot;https://github.com/Chrusic/todoist-mcp-server-extended&quot;&gt;this one&lt;/a&gt;, and installed it via &lt;a href=&quot;https://smithery.ai&quot;&gt;Smithery&lt;/a&gt;, which acts as a central repo for MCP servers.&lt;/p&gt;
&lt;p&gt;I like Smithery because their CLI makes installing MCPs super easy - all you need to do is run the following command:&lt;br&gt;
&lt;code&gt;npx -y @smithery/cli@latest install @Chrusic/todoist-mcp-server-extended --client claude&lt;/code&gt;&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;todoist-mcp-install.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Once installed, the only extra thing you’ll need is a Todoist API token:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log in to your &lt;a href=&quot;https://www.todoist.com/&quot;&gt;Todoist account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Navigate to &lt;code&gt;Settings&lt;/code&gt; → &lt;code&gt;Integrations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Find your API token under &lt;code&gt;Developer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Copy API Token&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Paste it when prompted, and you’re good to go.&lt;/p&gt;
&lt;p&gt;Open the &lt;a href=&quot;https://claude.ai/download&quot;&gt;Claude app&lt;/a&gt;, and behold - it can now access Todoist on your behalf.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;available-mcp-tools.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;watching-the-magic-happen&quot;&gt;Watching the Magic Happen&lt;/h2&gt;
&lt;p&gt;With the Todoist MCP server up and running and my API token in place, I handed the rest to Claude.&lt;/p&gt;
&lt;p&gt;I already used O1 to generate the execution plan earlier in the process. Now, all I had to do was give it a final prompt - this time, with instructions to take that structured task list and push it into Todoist using the MCP interface.&lt;/p&gt;
&lt;p&gt;Here’s the prompt I used:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;md&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;This is my OKR and my task list for hitting the goals in this OKR. I need your help in using your tools to put these tasks into my task-management app (Todoist).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;Here&apos;s what I need you to do:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Create Todoist Project called FY26H1 OKR.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Under this project, create a section for each KR (name it [KR Title] (KR [KR Number])).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; Finally, populate each section with the task from the task list. Put the DoD and the Description together in the task description.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;If you are not sure about something, don&apos;t assume my intent - ask me how to proceed.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude handled the whole thing in one go - no errors, no re-prompts.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;claude-creating-okr-tasks.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;It created the projects, slotted each task under the correct KR, and populated the fields precisely as instructed. Start dates, descriptions, labels, priorities - it all showed up as expected in Todoist.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;okr-project-todoist.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;There’s something very satisfying about opening the app and seeing a full quarter of structured, scoped, and scheduled work already laid out - without having to touch an API or spend an afternoon babysitting a migration script.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There’s something very satisfying about opening Todoist and seeing a full quarter of scoped, scheduled work already laid out - without touching an API, writing glue code, or falling into a planning spiral.&lt;/p&gt;
&lt;p&gt;But the bigger shift here isn’t just about automation. We’re used to thinking of LLMs as assistants - tools that can help us write code, summarize text, and generate emails. And they’re great at that.&lt;/p&gt;
&lt;p&gt;But this flow showed me that models can do more than assist - they can own operational pieces of your workflow. They can reason. Plan. Execute. It will not just help you work faster but also help you work smarter by pushing the low-leverage stuff off your plate entirely.&lt;/p&gt;
&lt;p&gt;And that’s the point. I don’t want to spend time manually scoping key results into tickets. I want to focus on the hard parts - the parts that actually require judgment, creativity, and experience. Let the model handle the project management overhead. I’ll show up and do the work.&lt;/p&gt;
&lt;p&gt;And the best part? This wasn’t some custom-built workflow with hardcoded paths and brittle dependencies. It’s just a good model, a standard interface, and a few lines of prompt. Which means I can do this again next quarter. And the one after that.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.aLJ1EnUW_ZE2goM.jpeg" medium="image"/><category>ai</category><category>mcp</category><category>career</category></item><item><title>I now use AI Agents to text you back</title><link>https://posts.oztamir.com/i-now-use-ai-agents-to-text-you-back/</link><guid isPermaLink="true">https://posts.oztamir.com/i-now-use-ai-agents-to-text-you-back/</guid><description>Automating Social Checkups with n8n, Claude, and a WhatsApp MCP</description><pubDate>Sun, 06 Apr 2025 11:15:39 GMT</pubDate><content:encoded>&lt;p&gt;As an ADHD person, I have a problem - I forget to get back to people.&lt;/p&gt;
&lt;p&gt;People will text me, and I’ll read it, maybe even think of a reply, and then just… won’t? For days? Sometimes weeks.&lt;/p&gt;
&lt;p&gt;It’s not that I’m avoiding anyone - on the contrary. It’s almost always people I like that I ghost. Friends, close family, people I genuinely want to keep in touch with. But if the conversation doesn’t require a reply right that second, odds are, I’ll forget.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;why-i-didnt-text-back.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;Many of my friends have got this image when I first &lt;a href=&quot;https://x.com/OzTamir/status/1880636601350725861&quot;&gt;saw it on X&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;So I figured: can this be automated? Can I build a system that knows who messaged me, who I haven’t replied to in a while, and gently reminds me to say hi before I have to begin my reply with “sorry for not getting back to you sooner”?&lt;/p&gt;
&lt;p&gt;Turns out: &lt;strong&gt;yes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And to make it, I used the hottest game in town - AI agents and MCPs.&lt;/p&gt;
&lt;h2 id=&quot;the-plan&quot;&gt;The Plan&lt;/h2&gt;
&lt;p&gt;This whole thing started when I stumbled across a new &lt;a href=&quot;https://github.com/lharries/whatsapp-mcp&quot;&gt;WhatsApp MCP server&lt;/a&gt; on GitHub. Naturally, I had to try it.&lt;/p&gt;
&lt;p&gt;Around the same time, I’d been using &lt;a href=&quot;https://n8n.io/&quot;&gt;n8n&lt;/a&gt; at work for some growth automation experiments at &lt;a href=&quot;https://blockaid.io/&quot;&gt;Blockaid&lt;/a&gt; - and I started wondering: what if I’d try to use this platform to solve personal problems too?&lt;/p&gt;
&lt;p&gt;So here was the plan:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Set up a self-hosted n8n instance on a Raspberry Pi&lt;/strong&gt;&lt;br&gt;
The &lt;a href=&quot;https://github.com/nerding-io/n8n-nodes-mcp&quot;&gt;MCP node for n8n&lt;/a&gt; only works on self-hosted instances. So I couldn’t just spin this up on the n8n Cloud.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up the WhatsApp MCP server and connect it to an LLM agent&lt;/strong&gt;&lt;br&gt;
The MCP server would sync my WhatsApp chats via the multi-device API, and expose a tool interface for querying messages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use this setup to pull a list of conversations I missed&lt;/strong&gt;&lt;br&gt;
I’d query the server for chats where the last message was from someone else - and I hadn’t replied in a while.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Send myself a summary with everyone I should probably get back to&lt;/strong&gt;&lt;br&gt;
Ideally, with a little LLM-generated nudge like “Here’s what they said. Want to say hi?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Once it works, turn it into a daily routine&lt;/strong&gt;&lt;br&gt;
Add a Cron trigger in n8n to run the whole flow each morning.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That was the idea. A little automation to help me be a better version of myself. Or at least a version who replies to messages before it gets awkward..&lt;/p&gt;
&lt;h2 id=&quot;setting-up-n8n&quot;&gt;Setting up n8n&lt;/h2&gt;
&lt;p&gt;Step one was getting a self-hosted n8n instance up and running on my Raspberry Pi.&lt;/p&gt;
&lt;p&gt;If you’re not familiar, &lt;a href=&quot;https://n8n.io&quot;&gt;n8n&lt;/a&gt; is a workflow automation tool - sort of like Zapier, but open-source, self-hostable, and way more powerful. It’s also AI native, which makes creating flows like what I wanted to achieve super easy.&lt;/p&gt;
&lt;p&gt;While the n8n team is offering hosted versions, these don’t support community nodes - extensions to n8n developed by the open source community (The &lt;a href=&quot;https://github.com/nerding-io/n8n-nodes-mcp?tab=readme-ov-file&quot;&gt;MCP node&lt;/a&gt; is one such node). To be able to run community nodes, one would have to run the self-hosted version of n8n.&lt;/p&gt;
&lt;p&gt;That was fine - I always have a spare Raspberry Pi lying around for emergencies like these. I quickly 3D printed a case and was ready to go.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;raspberry-pi-on-desk.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;For the actual setup, I followed this excellent guide, which walks you through installing n8n. It’s not anything special - basically installing Raspberry Pi OS, installing &lt;code&gt;node&lt;/code&gt;, and downloading &lt;code&gt;n8n&lt;/code&gt; through &lt;code&gt;npm&lt;/code&gt;. To make sure things are persistent, I used the &lt;code&gt;pm2&lt;/code&gt; setup that makes sure &lt;code&gt;n8n&lt;/code&gt; runs on boot.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-terminal-install.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;A good tip I got was to use &lt;code&gt;pm2&lt;/code&gt;’s configuration file to pass environment variables to &lt;code&gt;n8n&lt;/code&gt;. Specifically, I needed to give a parameter that allows insecure (http-based) access to the system. Since I only intend to access this setup locally for now, this is OK. To pass the variable, I used this &lt;code&gt;ecosystem.config.js&lt;/code&gt; setup:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;    apps &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        name   &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;n8n&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        env&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;            N8N_SECURE_COOKIE&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, I had &lt;code&gt;n8n&lt;/code&gt; running and ready to go - it was time to install the nodes.&lt;/p&gt;
&lt;h2 id=&quot;adding-mcp-support-to-n8n&quot;&gt;Adding MCP support to n8n&lt;/h2&gt;
&lt;p&gt;With n8n running, the next step was making it aware of my WhatsApp messages - or more specifically, giving it a way to ask about them.&lt;/p&gt;
&lt;p&gt;That’s where &lt;a href=&quot;https://github.com/lharries/whatsapp-mcp&quot;&gt;whatsapp-mcp&lt;/a&gt; comes in. This MCP server connects to your personal WhatsApp account (using &lt;a href=&quot;https://github.com/tulir/whatsmeow&quot;&gt;whatsmeow&lt;/a&gt;), and stores your messages in a local SQLite database. From there, it exposes an MCP Server which can be accessed using agents.&lt;/p&gt;
&lt;p&gt;An MCP Server is part of a new open standard called the &lt;a href=&quot;https://modelcontextprotocol.io/introduction#why-mcp%3F&quot;&gt;Model Context Protocol&lt;/a&gt; - a protocol designed to help AI agents interact with external tools and data sources. In practice, an MCP Server wraps a data source (like my WhatsApp messages), and exposes it to AI agents through a uniform interface.&lt;/p&gt;
&lt;p&gt;When connected to an agent (like Claude or ChatGPT), it can respond to tool calls like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“List messages I haven’t responded to in the past week.”&lt;br&gt;
“Summarize this chat thread.”&lt;br&gt;
“Draft a short reply.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because it’s just an interface layer, all the actual data stays local. And because it’s a standard, tools like n8n can now connect to any MCP server and use those tools inside workflows.&lt;/p&gt;
&lt;p&gt;Or at least, that’s the idea - at the time of writing, &lt;code&gt;n8n&lt;/code&gt; does not have a native MCP support. To connect &lt;code&gt;n8n&lt;/code&gt; to an MCP server, I first needed to install the &lt;a href=&quot;https://github.com/nerding-io/n8n-nodes-mcp&quot;&gt;n8n-nodes-mcp&lt;/a&gt; community node. This node adds support for calling tools through an MCP interface.&lt;/p&gt;
&lt;p&gt;Installing a new node is very easy - you can do it in the GUI:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-mcp-node-install-dialog.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Or simply by going to &lt;code&gt;~/.n8n/nodes&lt;/code&gt; and running &lt;code&gt;npm install n8n-nodes-mcp&lt;/code&gt;. Once the installation is complete, you can verify that the new node is up and running by navigating to &lt;code&gt;/settings/community-nodes&lt;/code&gt; and confirming that it shows up in the list:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-mcp-community-node-installed.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;installing-the-whatsapp-mcp-server&quot;&gt;Installing the WhatsApp MCP Server&lt;/h2&gt;
&lt;p&gt;With MCP support added to n8n, the next step was actually spinning up an MCP server that could serve my WhatsApp messages. Like I said, to make this connection I used &lt;a href=&quot;https://github.com/lharries/whatsapp-mcp&quot;&gt;whatsapp-mcp&lt;/a&gt; - a project that connects to your WhatsApp account and makes your chats queryable over MCP.&lt;/p&gt;
&lt;p&gt;The server is using a project called &lt;a href=&quot;https://github.com/tulir/whatsmeow?tab=readme-ov-file&quot;&gt;whatsmeow&lt;/a&gt;, which is written in Go, to connect to WhatsApp - so I first needed to install &lt;code&gt;golang&lt;/code&gt; on my Raspberry Pi.&lt;/p&gt;
&lt;p&gt;I’m not a go programmer, so take what I say with a grain of salt - I initially used &lt;code&gt;apt install golang&lt;/code&gt; to get it installed, but this method caused a mismatch with the projects expected go version.&lt;/p&gt;
&lt;p&gt;What I ended up doing is just getting the binaries that matched my version:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;wget&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; https://go.dev/dl/go1.24.1.linux-arm64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; tar&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; -C&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; /usr/local&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; -xzf&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; go1.24.1.linux-arm64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; PATH&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;$PATH&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;:/&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;usr&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;local&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;go&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;bin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;go&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; version&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt; # To verify that everything went well&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I cloned the repo:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; https://github.com/lharries/whatsapp-mcp.git&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; whatsapp-mcp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And started the bridge:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;go&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; ./whatsapp-bridge/main.go&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the first run, it displays a QR code in the terminal - you need to scan it with the WhatsApp app on your phone (it’s essentially the same process as pairing with WA Web):&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;whatsapp-bridge-qr-terminal.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;This QR code will let you log into my WhatsApp, you should scan it!&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Within a few seconds the sync started. From that point forward, the server quietly indexed all my messages into a local SQLite database.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;whatsapp-mcp-demo.gif&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;connecting-the-whatsapp-mcp-to-n8n&quot;&gt;Connecting the WhatsApp MCP to n8n&lt;/h2&gt;
&lt;p&gt;With the WhatsApp MCP server installed, the last step was registering it inside n8n so I could use its tools in workflows.&lt;/p&gt;
&lt;p&gt;Instead of using the HTTP interface, I opted to connect it through the STDIO API, which runs the MCP server locally as a subprocess inside n8n. This approach is fully supported by the &lt;code&gt;n8n-nodes-mcp&lt;/code&gt; node, and has the added benefit of not requiring a separate always-on process - n8n can spin it up as needed.&lt;/p&gt;
&lt;p&gt;To do that, I first went to the Credentials section in n8n, created a new credential of type MCP Client (STDIO) API, and filled in the following (taken from the &lt;code&gt;whatsapp-mcp&lt;/code&gt; docs):&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;whatsapp-mcp-credential-config.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This tells n8n to use &lt;code&gt;uv&lt;/code&gt; to run the WhatsApp MCP server locally via its Python entrypoint. Since the server is structured as a Python module, this works out of the box.&lt;/p&gt;
&lt;p&gt;I saved the credential and tested the connection - and just like that, n8n was able to discover the available tools exposed by the WhatsApp MCP.&lt;/p&gt;
&lt;p&gt;Now, when adding an MCP node to a workflow, I could select the new “WhatsApp MCP” credential, choose a tool, and pass in any parameters or context I wanted.&lt;/p&gt;
&lt;p&gt;Next up: building a workflow that actually uses these tools to figure out who I’ve been ignoring.&lt;/p&gt;
&lt;h2 id=&quot;building-an-example-workflow&quot;&gt;Building an Example Workflow&lt;/h2&gt;
&lt;p&gt;With the MCP credential set up and the tools exposed in n8n, it was time to build the actual workflow.&lt;/p&gt;
&lt;p&gt;To start simple, I made a basic agent flow that could receive a message and call tools from the WhatsApp MCP server. The setup looks something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A chat trigger node that kicks things off when I message the agent&lt;/li&gt;
&lt;li&gt;An AI Agent node, configured with Claude 3 and given strict instructions to never send a message (just make suggestions)&lt;/li&gt;
&lt;li&gt;A memory node to give the agent short-term recall&lt;/li&gt;
&lt;li&gt;Two MCP tool nodes wired into the agent - one to register the available WhatsApp tools, and one to actually execute them&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;n8n-agent-workflow-diagram.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Here’s the system message I gave the agent:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;You&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; are&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; a&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; helpful&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; assistant&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; designed&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; help&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; the&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; manage&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; their&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; social&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; interactions&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; on&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; WhatsApp.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;Whatever&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; you&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; do,&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; you&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; must&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; never&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; send&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; message.&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; Do&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; do&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; it.&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; No&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; matter&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; what.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;For&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; context,&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; the&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; current&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; date&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; is:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; {{&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; Date&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;.toDateTime&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; }}.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To test this setup, I started small - I asked the agent to check what was the latest message I got from my partner. I had asked her to close the bedroom window, and hold and behold - the bot caught it instantly.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;agent-chat-fetch-message.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The integration is working! Then came the real test: Could it figure out who I genuinely needed to respond to?&lt;/p&gt;
&lt;p&gt;So I asked it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Go over all my chats from the past 72 hours. Anything I missed? Please reply in a prioritized list of items I should look into (if I should reply - it should be higher on the list).&lt;br&gt;
Give me 10 items at most (ideally as few items as possible).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And the result was…very good?!&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;agent-chat-priority-list.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;it pulled a few one-on-one threads where I had clearly dropped the ball, summarized the last thing said, and even proposed short replies. It’s working!&lt;/p&gt;
&lt;h2 id=&quot;next-steps&quot;&gt;Next steps&lt;/h2&gt;
&lt;p&gt;With this proof-of-concept working end-to-end - from syncing chats, to querying missed messages, to getting back helpful suggestions - the next steps are to take this toy project and turn it into a “production” application.&lt;/p&gt;
&lt;p&gt;To meet this criteria, here are the next items on my to-do list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Run this workflow periodically&lt;/strong&gt; - The obvious first step. &lt;code&gt;n8n&lt;/code&gt; makes this very easy - and I plan to have this workflow run every morning to ensure I don’t miss anyone over too long of a time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actually deliver the results&lt;/strong&gt; - Once I have a list of chats I should get back to, it should probably… go somewhere. Since we already have the WhatsApp MCP here, sending a WhatsApp reminder to myself seems like the easiest way to go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interact with a reminders MCP&lt;/strong&gt; - I already saw people creating MCPs for to-do apps like &lt;a href=&quot;https://github.com/abhiz123/todoist-mcp-server&quot;&gt;Todoist&lt;/a&gt;. By adding in this integration (which, again, should be very easy), I could have the agent not only tell me to reply, but schedule a reminder that will bug me again later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;(Maybe) generate suggested replies&lt;/strong&gt; - Right now the agent tells me who to respond to, and what they said. Eventually, it could also suggest what I might want to say.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;summary&quot;&gt;Summary&lt;/h1&gt;
&lt;p&gt;The combination of agents and MCPs opens up a lot of new possibilities - and as a technical person, it’s hard not to get excited about that.&lt;/p&gt;
&lt;p&gt;This wasn’t a huge project. Just a weekend’s worth of curiosity, some open source code, and a Raspberry Pi that was already sitting around. But it solved a real problem in my life, and it gave me a glimpse into what building with these tools can feel like when everything actually clicks.&lt;/p&gt;
&lt;p&gt;I think everyone should be experimenting with this stuff - whether it’s a small, personal automation like this, or something bigger at work. AI is going to change the way we build software, and the engineers who stay curious and hands-on are the ones who are going to stay ahead.&lt;/p&gt;
&lt;p&gt;And if you can explore this space while also becoming slightly less bad at replying to your friends - why not?&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.B6vZmzKV_VJ2BS.jpeg" medium="image"/><category>ai</category><category>agents</category><category>mcp</category><category>n8n</category><category>automation</category><category>Technical</category></item><item><title>Building the MDR terminal from Severance</title><link>https://posts.oztamir.com/building-the-lumon-mdr-terminal-from-severance/</link><guid isPermaLink="true">https://posts.oztamir.com/building-the-lumon-mdr-terminal-from-severance/</guid><description>I decided to make my own Lumon Macrodata Refinement terminal.</description><pubDate>Sat, 15 Feb 2025 09:34:26 GMT</pubDate><content:encoded>&lt;p&gt;Like a lot of folks, I recently got hooked on Severance. This wasn’t my first time hearing about the show—back when Season 1 was running, people wouldn’t shut up about it. I even gave it a shot back then, watched a single episode, and decided it was way too dark for my taste.&lt;/p&gt;
&lt;p&gt;That was a mistake.&lt;/p&gt;
&lt;p&gt;With all the buzz leading up to Season 2, I figured I’d give it another chance. And wow—what a fantastic piece of television. I binged all the available episodes, and now I’m just sitting here, restless, waiting for the next one to drop.&lt;/p&gt;
&lt;p&gt;Which means I needed something to do in the meantime.&lt;/p&gt;
&lt;p&gt;See, when I get into a new piece of pop culture, I don’t just watch it—I dive in. Reading theories on Reddit, forcing my friends to watch along, the whole thing. But even that’s not enough. My brain won’t rest until I’ve made something out of it. It’s how I feel like I’m part of it. Some people buy merch—I build.&lt;/p&gt;
&lt;p&gt;So, naturally, I decided to make my own MDR terminal.&lt;/p&gt;
&lt;h2 id=&quot;physical-parts&quot;&gt;Physical Parts&lt;/h2&gt;
&lt;p&gt;To get started, I needed to build the physical enclosure of the MDR terminal. Instead of modeling it from scratch, I used this MagSafe charger modal from &lt;a href=&quot;https://makerworld.com/en/models/1068075#profileId-1057392&quot;&gt;@SeoulBrother&lt;/a&gt; - with slight modifications.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;printables-severance-dock-listing.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;While the original modal is made to fit an iPhone and therefore is not hollow, I knew that I would need a lot of storage space for the electronics - so I used a negative cube to carve out a big chunk of the case. I will not upload this modified part to avoid breaking the license on the original upload - but here is how it looks like in my slicer. YMMY though.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;cad-model-slicer.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Next, I needed to recreate the front-facing part of the enclosure. To do so, I looked up promotional images from the series - luckily for me, Apple had run a &lt;a href=&quot;https://forums.macrumors.com/threads/apple-continues-promoting-severance-ahead-of-season-2-premiere.2447547/&quot;&gt;marketing campaign&lt;/a&gt; for the show which included an Apple-like product page for Lemon’s stuff. This gave me this great reference image:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;lumon-promo-terminal.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Based on this reference, I fired up Fusion 360 and started modeling. Usually, when modeling off a reference image, I’ll start from tracing the features of the image - this time, I started with the screen dimensions and traced based on the enclosure size I already had - which is why the proportions are a bit off.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;fusion360-keypad-top-view.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Of the flip side (literally…), I modeled a holder for the combined unit of the Raspberry Pi + screen:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;cad-base-with-screen-cutout.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;One of the biggest reasons I am a strong believer in 3D printing is just how easy this manufacturing process is, and how this enables lighting fast iterations - within the two evenings I spent on this project, I went through quite a few iterations.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;printed-parts-disassembled.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;One of the biggest issues I had was making sure that the Pi stays in place and is not pushed back. After trying multiple approaches, I ended up with placing overhangs above the unit and adding holes for fixing it in place with nuts and bolts.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;screwdriver-on-pi-board.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;For anyone interested in building this themselves - the final modal (and the Fusion project file!) is available for download &lt;a href=&quot;https://www.printables.com/model/1192369-lumon-mdr-terminal-faceplate-severance&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Another issue I faced was the tight space for cables. If you’ve ever worked on a hobby project that was tight in space, you probably noticed the same thing as I did in this one - cables are f**king huge. The connector for USB-C? Giant! Where am I supposed to find a place for this?&lt;/p&gt;
&lt;p&gt;No. Instead, I went ahead and got an angled cable, which is apparently a thing. I was very happy to discover that this is a solved problem, because I was inches away from splicing the cable and trying to make this on my own.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;raspberry-pi-in-blue-shell.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;As a final touch, I added a small Pi-connected camera that I got a while back, accidentally broke, and never used again.&lt;/p&gt;
&lt;h2 id=&quot;electronics-and-software&quot;&gt;Electronics and Software&lt;/h2&gt;
&lt;p&gt;Right from the outset, It was clear to me that this is the type of project where I am better off with a Raspberry Pi rather than trying to use an Arduino or ESP platform - I know that I’ll need to run some heavy graphics, and I didn’t want to be strapped for resources.&lt;/p&gt;
&lt;p&gt;Due to the small space constraints, I opted for a Raspberry Pi Zero - the small form factor made sense to me, and I thought that while the graphics are probably a lot for an ESP, the Zero should be able to handle the load. Using a full-form Pi seemed wasteful.&lt;/p&gt;
&lt;p&gt;For the screen, I pulled an old touch display I had laying around for years. This display uses SPI for communications, and it seemed like a good fit. I connected the two, and started working on the software side of things.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;pi-tft-display-wired.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;However, after some tinkering around, It turned out that both of these decisions were ill guided. The display, which is based on a driver called &lt;code&gt;IL9488&lt;/code&gt;, turned out to be very badly supported - the required drivers were removed from the Pi main kernel back in 2015. While there &lt;em&gt;are&lt;/em&gt; a couple of projects that claim to solve this by providing their own drivers, after compiling a few It was clear that this avenue will be more hassle than reward - which is why I decided to drop it and just get a &lt;a href=&quot;https://www.waveshare.com/wiki/3.5inch_RPi_LCD_(A)&quot;&gt;Waveshare LCD screen&lt;/a&gt; instead.&lt;/p&gt;
&lt;p&gt;However, this screen was now using HDMI instead of SPI. While, again, it looked like it is &lt;em&gt;possible&lt;/em&gt; to run this on a Zero using SPI, I did not want to invest any more time on this - so I just switched over to a Pi 4, which played nicely and was supported by the provided HDMI to Mini HDMI dongle.&lt;/p&gt;
&lt;p&gt;At the end of the day, I think this switch was the right call - it went from being a shit show to “just work”. Since time is money, I am quite happy with this trade.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;screen-boot-log-partial.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;software&quot;&gt;Software&lt;/h2&gt;
&lt;p&gt;Since I was using a Raspberry Pi, I figured that most of the software is already out there - and I was right. A quick search brought me to the Github repository for &lt;a href=&quot;https://github.com/guysoft/FullPageOS?tab=readme-ov-file&quot;&gt;FullPageOS&lt;/a&gt; - a Raspbian fork customized to support kiosk mode, where you boot the device and it automatically loads up a web browser pointed to a website of your choosing.&lt;/p&gt;
&lt;p&gt;For the actual MDR software, I used &lt;a href=&quot;https://github.com/Lumon-Industries/Macrodata-Refinement&quot;&gt;this&lt;/a&gt; project by the &lt;em&gt;fake&lt;/em&gt; Lumons Industries Github page. This project is actually, and uncharacteristically, just raw Javascript - which meant that I didn’t need to play around with any fancy building solutions to run this on the Pi.&lt;/p&gt;
&lt;p&gt;To get it running, I simply created a small nginx server and pointed the FullPageOS config to the server. First, I needed to turn off the existing HTTP server that ships with the distro:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; systemctl&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; stop&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; lighttpd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; systemctl&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; disable&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; lighttpd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, I installed ngnix and cloned the repo:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; apt&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; update&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; apt&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; nginx&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; https://github.com/Lumon-Industries/Macrodata-Refinement&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; mdr&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I had everything I needed, I edited the ngnix config (&lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt;) to serve the project files:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;server {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    listen 80;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    server_name _;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    root /home/pi/mdr;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    index index.html;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    location / {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        try_files $uri $uri/ =404;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I made sure that the server is running with the new configuration:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sudo systemctl restart nginx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sudo systemctl enable nginx&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And indeed, serving to the Pi’s IP address gave me the MDR interface:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;mdr-game-ui-numbers.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The only missing piece was to set the FullPageOS configuration to point to the site. To edit the URL loaded on boot, all needed is to edit a simple file:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;rm /boot/firmware/fullpageos.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;echo &apos;http://localhost/&apos; &gt; /boot/firmware/fullpageos.txt&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;final-result&quot;&gt;Final Result&lt;/h2&gt;
&lt;p&gt;Once all the printed parts, the electronics, and the software were all ready, it was time to put it all together. This took some back and forth, especially around getting the electronics to fit in the case.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;pi-inside-white-shell.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;In fact, I ended up having to break a tiny bit of the printed part to fit it in. While I fixed it in the final modal I published, I didn’t want to reprint - which is why the resulting terminal is using a broken part (as you can see in the “bolt” image above).&lt;/p&gt;
&lt;p&gt;However, it was all worth it. Because the final result is stunning.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;finished-terminal-side-view.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;terminal-with-keyboard-angle.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I have to say, looking at my completed MDR terminal sitting on my desk, I feel… oddly proud?&lt;/p&gt;
&lt;p&gt;It’s weird to put this much effort into recreating a computer designed for soul-crushing corporate monotony, but here we are.&lt;/p&gt;
&lt;p&gt;When I told some friends that I was working on this project, I got a recurring question - “but what would you do with this?”.&lt;/p&gt;
&lt;p&gt;The honest answer? I’ll probably keep it for a while on my desk and then either dismantle it or store it in the closet somewhere.&lt;/p&gt;
&lt;p&gt;I know that this is a bit underwhelming, but the truth of the matter is that I do these projects for the sake of the project - not for some “goal”. This is what makes this a hobby - and not a job.&lt;/p&gt;
&lt;p&gt;But than again - maybe, like the folks at Lumon, I do have a hidden goal here. Only Kier knows.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.DEYqY25l_INQT5.jpeg" medium="image"/><category>3D Printing</category><category>Diy</category><category>electronics</category><category>RaspberryPi</category><category>Culture</category><category>TV</category></item><item><title>Beating the Cold Room Syndrome with Zigbee</title><link>https://posts.oztamir.com/beating-the-cold-room-syndrome-with-zigbee/</link><guid isPermaLink="true">https://posts.oztamir.com/beating-the-cold-room-syndrome-with-zigbee/</guid><description>I kept forgetting to turn on the heater before I shower—until I decided to automate it once and for all.</description><pubDate>Tue, 11 Feb 2025 21:40:53 GMT</pubDate><content:encoded>&lt;p&gt;One of the feelings I hate the most around winter time is the striking temperature difference between my shower and the outside world.&lt;/p&gt;
&lt;p&gt;Whenever I’m done washing up, I dread the moment when I’ll have to get outside of the bathroom and into my bedroom - which is often very cold, especially in comparison.&lt;/p&gt;
&lt;p&gt;Sure, there’s an obvious fix: turn on the bedroom AC before I shower.&lt;/p&gt;
&lt;p&gt;But here’s the thing—I never remember to do that. Not before stepping in, anyway. I only ever remember the second I step out, which, as you can imagine, is a little too late.&lt;/p&gt;
&lt;p&gt;Finally, after one too many of these experiences, I decided enough was enough. It was time to automate my way out of misery.&lt;/p&gt;
&lt;h2 id=&quot;honey-are-you-in-the-shower&quot;&gt;Honey, are you in the shower?&lt;/h2&gt;
&lt;p&gt;The solution to this pain point is pretty easy - I just need to figure out when I shower, and trigger the bedroom AC. I already own a Sensibo, so the only hard part is the shower detection.&lt;/p&gt;
&lt;p&gt;This is kind of a fickle problem, because I would not want to have any sort of optical solution - no cameras in the bathroom, thank you very much.&lt;/p&gt;
&lt;p&gt;Another option I briefly considered using a motion sensor, but that would trigger every time someone walked into the bathroom—not just when someone was showering. A timed schedule was also a bad option - it’s too rigid.&lt;/p&gt;
&lt;p&gt;I wanted something smarter; something that knows I’m showering without creeping on me. Luckily for me, I had the perfect device for this use case laying around.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;zigbee-sensor-wall-mounted.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;tracking-showers-using-zigbee-humidity-sensor&quot;&gt;Tracking showers using Zigbee Humidity sensor&lt;/h2&gt;
&lt;p&gt;I originally bought the &lt;a href=&quot;https://sonoff.tech/product/gateway-and-sensors/snzb-2d/&quot;&gt;SONOFF’s Humidity and Temperature Sensor&lt;/a&gt; (SNZB-02D) to try and help with fan control (our bathroom has an humidity problem…), but ditched it when I figured out that the fan should just be always on.&lt;/p&gt;
&lt;p&gt;But now, I finally found a usage for it - I can use it to try and detect sharp changes in the humidity of the bathroom to detect when one of us is in the shower.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;sonoff-device-info-zigbee2mqtt.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;After setting up the sensor and pairing it with Home Assistant (via &lt;a href=&quot;https://www.home-assistant.io/integrations/zha/&quot;&gt;ZHA&lt;/a&gt;), we can take a look at the humidity history:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;humidity-two-day-graph.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;It’s not hard to see that there are obvious upticks in the graph, and these indeed map directly to our recent showers. Looking over the last month of data, the pattern was very clear—whenever someone showered, the humidity spiked past 85%.&lt;/p&gt;
&lt;p&gt;It was a clean, repeatable cutoff, which meant that the solution was very easy - all I need is to set my automation to trigger when it crossed that threshold.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;humidity-eleven-day-graph.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;I should note that I’ve played around with derived sensors to try and see if we can optimize this flow (I assumed that by measuring the rate of change we might be able to detect a shower faster, thus giving us more time to heat up the room). However, these did very little to help, which is why I decided to stick with the simple solution.&lt;/p&gt;
&lt;p&gt;What I &lt;em&gt;did&lt;/em&gt; do was to set this automation with a bunch of extra conditions to help ensure that this is not triggered randomly if the humidity is too high - I ensured that the timeframe makes sense, that there’s someone home, etc.&lt;/p&gt;
&lt;p&gt;So, how does the final automation looks like?&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; Turn on Bathroom AC when someone showers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;triggers&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; trigger&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; numeric_state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    entity_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      -&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; sensor.bathroom_temperature_sensor_humidity&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    above&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 85&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;conditions&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; condition&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    after&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;17:00:00&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    before&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;10:00:00&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; condition&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    entity_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; binary_sensor.someone_is_home&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    state&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; condition&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; numeric_state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    entity_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; climate.bedroom&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    attribute&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; current_temperature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    below&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 20&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;actions&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; action&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; sensibo.full_state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    metadata&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    data&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      mode&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; heat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      target_temperature&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 24&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      fan_mode&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; strong&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    target&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      device_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;ID&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; action&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; sensibo.enable_timer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    metadata&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    data&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      minutes&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    target&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      device_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;ID&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; single&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As always, a little automation goes a long way.&lt;/p&gt;
&lt;p&gt;I no longer have to remember to turn on the heater before I shower—Home Assistant does it for me.&lt;/p&gt;
&lt;p&gt;No more shivering, no more rushed dives under the blanket, just a warm, comfortable room waiting for me when I step out.&lt;/p&gt;
&lt;p&gt;To be honest, these kind of projects are my favorites. It’s not about the flashy, over-the-top automations—it’s about solving tiny annoyances that make life just a little better.&lt;/p&gt;
&lt;p&gt;And, as a bonus, it finally gave me a use for that humidity sensor I impulse-bought months ago.&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.B1gkFJ_Z_Z1MYxzt.jpeg" medium="image"/><category>automation</category><category>home-automation</category><category>smart-home</category><category>home-assistant</category></item><item><title>Syncing Home Assistant States to Exist.io</title><link>https://posts.oztamir.com/syncing-home-assistant-states-to-exist_io/</link><guid isPermaLink="true">https://posts.oztamir.com/syncing-home-assistant-states-to-exist_io/</guid><description>Tracking Home Assistant Data in Exist.io: Custom Integrations Made Easy</description><pubDate>Mon, 04 Nov 2024 16:03:32 GMT</pubDate><content:encoded>&lt;p&gt;Recently, I dove head-first into a new hobby - the world of quantified self. If you haven’t heard this phrase before, it’s a really simple idea - track everything.&lt;/p&gt;
&lt;p&gt;Steps, sleep, health, productivity, music listened to, food eaten. You name it.&lt;/p&gt;
&lt;p&gt;The attraction was clear - like any good engineer, I wanted to see if I could optimize my life with a little data and some well-placed automation. &lt;a href=&quot;https://www.amazon.com/How-Measure-Anything-Intangibles-Business/dp/1118539273&quot;&gt;&lt;em&gt;How to Measure Anything&lt;/em&gt;&lt;/a&gt;, taken to the max.&lt;/p&gt;
&lt;p&gt;Of course, tracking is only half the battle.&lt;/p&gt;
&lt;p&gt;I wanted to figure out why certain patterns emerged: did late-night work affect my sleep? Did productivity take a hit when I had fast food the day before?&lt;/p&gt;
&lt;p&gt;I started searching for a tool that could help me bring all these pieces together—something that would do more than just count steps or track hours. And that’s when I found &lt;a href=&quot;#syncing-home-assistant-states-to-existio&quot;&gt;Exist.io&lt;/a&gt;, a service that promised to make sense of all this data.&lt;/p&gt;
&lt;p&gt;Exist.io connects to all kinds of apps for fitness, sleep, weather, even productivity, giving you an overview of how these daily factors stack up and interact.&lt;/p&gt;
&lt;p&gt;Perfect, right? Well, almost.&lt;/p&gt;
&lt;p&gt;Because once I started tracking, I realized there were still things I wanted to log that Exist didn’t support out of the box. The most pressing issue was that I was already gathering a &lt;a href=&quot;/whose-turn-is-it-any-way-tracking-with-zigbee-espresense-openepaperlink/&quot;&gt;bunch of data&lt;/a&gt; in Home Assistant - I wanted to use this data to enrich every insight I could get. But alas, there is no native Home Assistant integration in Exist.io.&lt;/p&gt;
&lt;p&gt;The good news - they have an &lt;a href=&quot;https://developer.exist.io/&quot;&gt;API&lt;/a&gt;, built for cases just like this one. With a little tweaking, I could create custom integrations to add my own data to Exist—bridging the gap and making sure every data I can find in Home Assistant can also be tracked on Exist.&lt;/p&gt;
&lt;p&gt;Since I couldn’t find any walkthrough of how to do this sync online, I figured I’d share my system for anyone interested in doing something similar.&lt;/p&gt;
&lt;p&gt;For this example, we’re going to be tracking how much 3D printing I’ve been doing each day, using the &lt;a href=&quot;github.com/greghesp/ha-bambulab&quot;&gt;Bambu HA integration&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;exist-api-overview&quot;&gt;Exist API Overview&lt;/h2&gt;
&lt;p&gt;Before we dive into syncing data, let’s get familiar with the Exist.io API basics, as these will be key for integrating Home Assistant with Exist.&lt;/p&gt;
&lt;p&gt;Exist.io revolves around attributes—metrics like steps, sleep hours, or, in our case, custom stats.&lt;/p&gt;
&lt;p&gt;Attributes in Exist are tracked daily, which means it’s designed for summarizing data by day rather than capturing every minute change.&lt;/p&gt;
&lt;h3 id=&quot;key-concepts&quot;&gt;Key Concepts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Attributes: These are the fundamental data points tracked in Exist.io. Each attribute has:
&lt;ul&gt;
&lt;li&gt;Name: A simple identifier like steps or daily_print_time (used in API requests).&lt;/li&gt;
&lt;li&gt;Label: A user-friendly title displayed in Exist, like “Daily Print Time.”&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.exist.io/reference/authentication/oauth2/#choosing-scopes&quot;&gt;Group&lt;/a&gt;: Exist organizes attributes into broad categories like Productivity, Sleep, or Weather.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.exist.io/reference/object_types/&quot;&gt;Value Type&lt;/a&gt;: Exist supports various data types for attributes, including integers, floats, strings, and durations (typically measured in minutes).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Custom Attributes: While Exist comes with a range of built-in attributes (e.g., sleep, workouts), you can create your own using the API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this example, we’ll define &lt;code&gt;daily_print_time&lt;/code&gt; as a custom attribute to log 3D printing activity each day.&lt;/p&gt;
&lt;p&gt;To access Exist’s API, we need to set up an &lt;a href=&quot;https://developer.exist.io/reference/authentication/oauth2/#overview&quot;&gt;OAuth2&lt;/a&gt; client app. This involves &lt;a href=&quot;https://exist.io/account/apps/&quot;&gt;creating an app&lt;/a&gt; on Exist.io, and using a token to authenticate Home Assistant with Exist.io:&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;exist-oauth-app-setup.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Once the app is created, you will get an access token that will be used to authenticate the API calls done by Home Assistant.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;exist-developer-token.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;making-home-assistant-talk-to-existio&quot;&gt;Making Home Assistant Talk to Exist.io&lt;/h2&gt;
&lt;p&gt;With Exist’s API basics covered, it’s time to set up the commands that will let Home Assistant communicate with Exist.io. To do so, we will be using &lt;a href=&quot;https://home-assistant.io/integrations/rest_command&quot;&gt;REST commands&lt;/a&gt;, an integration which allows HA to make HTTP calls.&lt;/p&gt;
&lt;p&gt;To send data to Exist, we will create a different command for each operation we want to be doing - creating a new attribute, updating its value, and incrementing it by a specified amount.&lt;/p&gt;
&lt;h4 id=&quot;create-attribute&quot;&gt;Create Attribute&lt;/h4&gt;
&lt;p&gt;The first step is to create our custom attribute in Exist.io, like &lt;code&gt;daily_print_time&lt;/code&gt;. This command only needs to be run once per attribute to set it up in your Exist account.&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;rest_command&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;  create_exist_attribute&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;  	url&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;https://exist.io/api/2/attributes/create/&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    method&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; POST&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    headers&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      Authorization&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Bearer YOUR_ACCESS_TOKEN&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      Content-Type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;application/json&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    payload&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;      [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;        {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;          &quot;label&quot;: &quot;{{ label }}&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;          &quot;group&quot;: &quot;{{ group }}&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;          &quot;value_type&quot;: {{ value_type }},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;          &quot;manual&quot;: false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;      ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this command:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;label&lt;/code&gt; is the name displayed in Exist.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group&lt;/code&gt; organizes the attribute under a category, like &lt;em&gt;productivity&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value_type&lt;/code&gt; defines the data type (3 here, meaning a duration in minutes).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;update-attribute&quot;&gt;Update Attribute&lt;/h4&gt;
&lt;p&gt;The next REST command allows you to set a specific value for the attribute on a given day. This can be useful if you need to overwrite an existing daily value or set it manually.&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;rest_command&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	update_exist_attribute&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  url&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;https://exist.io/api/2/attributes/update/&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  method&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; POST&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  headers&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	    Authorization&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Bearer YOUR_ACCESS_TOKEN&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	    Content-Type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;application/json&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  payload&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	    [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	      {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;{{ attribute }}&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;{{ new_state }}&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;{{ now().strftime(&apos;%Y-%m-%d&apos;) }}&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this command:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; specifies the attribute to update (in this case, &lt;code&gt;daily_print_time&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;date&lt;/code&gt; sets the day the value applies to, using today’s date.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt; is the new value we want to set this attribute to.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;increment-attribute&quot;&gt;Increment Attribute&lt;/h4&gt;
&lt;p&gt;The last REST command increments the attribute value by a specified amount. This command is particularly useful for tracking cumulative stats like daily printing time.&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;rest_command&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	increment_exist_attribute&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  url&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;https://exist.io/api/2/attributes/increment/&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  method&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; POST&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  headers&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	    Authorization&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Bearer YOUR_ACCESS_TOKEN&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	    Content-Type&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;application/json&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;	  payload&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; &gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	    [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	      {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;{{ attribute }}&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {{&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;{{ date | default(now().strftime(&apos;%Y-%m-%d&apos;)) }}&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;	    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; identifies the attribute to be incremented.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt; specifies the amount to increase the current day’s value by.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;date&lt;/code&gt; sets the day the value applies to, using today’s date.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With these commands in place, Home Assistant can now create, update, and increment Exist.io attributes directly, allowing you to push custom data from your setup into Exist.&lt;/p&gt;
&lt;h2 id=&quot;creating-an-attribute&quot;&gt;Creating an Attribute&lt;/h2&gt;
&lt;p&gt;With the REST commands set up, the next step is to create the custom attribute in Exist.io.&lt;/p&gt;
&lt;p&gt;This is the one-time setup that registers our attribute—daily print time—in your Exist account. This can be done either using an automation, or using the Home Assistant developer tools. Since this is a tutorial, let’s go with the latter:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;em&gt;Developer Tools&lt;/em&gt; in Home Assistant and go to the &lt;strong&gt;Actions&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Under Action, select &lt;code&gt;rest_command.create_exist_attribute&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Hit Perform Action to execute the command, creating the attribute in Exist.io.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;ha-rest-command-create-attribute.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Based on the response, you can tell if the attribute was successfully created, Once it is, we can go ahead and create an automation to log 3D printing time in Exist.&lt;/p&gt;
&lt;h2 id=&quot;logging-3d-printing-time-to-existio&quot;&gt;Logging 3D Printing Time to Exist.io&lt;/h2&gt;
&lt;p&gt;With the &lt;code&gt;daily_print_time&lt;/code&gt; attribute set up, we’re ready to start logging our 3D printing time.&lt;/p&gt;
&lt;p&gt;For this, we’ll create an automation that gets called whenever a print starts, and increments the attribute in Exist.io based on how much print time is planned. This is done using the &lt;code&gt;bambu_x1c_remaining_time&lt;/code&gt; attribute from the Bambu integration.&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; Log 3D Printing Time to Exist.io&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Automatically logs 3D printing time to Exist.io as the printer runs&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;trigger&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; platform&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    entity_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; sensor.ozs_bambu_x1c_remaining_time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    from&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    to&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;running&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;  # Adjust based on the sensor state representing a running printer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;condition&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; condition&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; numeric_state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    entity_id&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; sensor.ozs_bambu_x1c_remaining_time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    above&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;action&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; service&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; rest_command.increment_exist_attribute&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    data_template&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;daily_print_time&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      value&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;{{ trigger.to_state.state | int }}&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#616E88&quot;&gt;  # Replace with duration sensor if available&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  -&lt;/span&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt; service&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; logbook.log&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;    data&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Exist.io Log&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;      message&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Incremented print time on Exist.io by {{ trigger.to_state.state }} minutes&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8FBCBB&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; single&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make things debugable, we will also be logging the response into the logbook service.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;ha-automation-log-exist-sync.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Once this automation is active, you’ll start seeing your daily 3D printing time accumulate in Exist.io.&lt;/p&gt;
&lt;p&gt;With each print session, Home Assistant updates the &lt;code&gt;daily_print_time&lt;/code&gt; attribute, providing you with a running total that you can view alongside other life metrics.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;exist-daily-print-time-card.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;And that’s it—my 3D printing time is now tracked, quantified, and rolled into Exist.io along with my steps, sleep, and the rest of my life metrics.&lt;/p&gt;
&lt;p&gt;It’s honestly pretty satisfying to see data from this setup nestled right next to all the other insights Exist provides.&lt;/p&gt;
&lt;p&gt;Not to mention, it’s also a bit addictive; once you see how easy it is to pull in one data source, it’s tempting to start thinking of all the other things you could track.&lt;/p&gt;
&lt;p&gt;Of course, Exist.io is designed for daily summaries, so you won’t be analyzing every moment your lights turn on or every change in room temperature.&lt;/p&gt;
&lt;p&gt;But that’s fine—this integration is perfect for capturing high-level stats that build into trends over time. And if you’re like me, you’ll start finding connections you didn’t expect.&lt;/p&gt;
&lt;p&gt;Printing projects affecting your productivity? Sleep quality tied to home energy usage? Sure, why not.&lt;/p&gt;
&lt;p&gt;So, if you’re the kind of person who loves to track, tinker, and optimize, integrating Home Assistant with Exist.io is a solid move.&lt;/p&gt;
&lt;p&gt;It’s a great way to pull in unique data points that really matter to you, bringing them into the bigger picture of your daily life. An remember - if it can be tracked, it can be improved. So, why not?&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.DVwFDJAP_1RhOTV.jpeg" medium="image"/><category>Home Assistant</category><category>Quantified Self</category></item><item><title>Using EIP-6963 for Marketing</title><link>https://posts.oztamir.com/using-eip-6963-for-marketing/</link><guid isPermaLink="true">https://posts.oztamir.com/using-eip-6963-for-marketing/</guid><description>EIP-6963 isn’t just another Ethereum standard—it’s a powerful tool for building trust. We used it for social proof.</description><pubDate>Sat, 19 Oct 2024 21:08:00 GMT</pubDate><content:encoded>&lt;p&gt;Working in a Web3 centered &lt;a href=&quot;https://blockaid.io&quot;&gt;company&lt;/a&gt;, a lot of my online attention is spent on Twitter (X) - after all, this is where a lot of the people in the community are.&lt;/p&gt;
&lt;p&gt;A few days ago, I was browsing the feed when I came across the impressive work that &lt;a href=&quot;https://zerion.io/&quot;&gt;Zerion&lt;/a&gt; is doing with their &lt;a href=&quot;https://www.unblockcrypto.org/&quot;&gt;UnblockCrypto&lt;/a&gt; campaign.&lt;/p&gt;
&lt;p&gt;This campaign aims to promote the adoption of a new Ethereum standard, EIP-6963.&lt;/p&gt;
&lt;h2 id=&quot;eip-6963&quot;&gt;EIP-6963&lt;/h2&gt;
&lt;p&gt;For those unfamiliar, &lt;a href=&quot;https://eips.ethereum.org/EIPS/eip-6963&quot;&gt;EIP-6963 (Multi Injected Provider Discovery)&lt;/a&gt; is a community-driven standard that introduces an alternative discovery mechanism to &lt;code&gt;window.ethereum&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As written in the EIP document:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Currently, Wallet Provider that offer browser extensions must inject their Ethereum providers (EIP-1193) into the same window object window.ethereum; however, this creates conflicts for users that may install more than one browser extension.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;This results not only in a degraded user experience but also increases the barrier to entry for new browser extensions as users are forced to only install one browser extension at a time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;In this proposal, we present a solution that focuses on optimizing the interoperability of multiple Wallet Providers. This solution aims to foster fairer competition by reducing the barriers to entry for new Wallet Providers, along with enhancing the user experience on Ethereum networks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;This is achieved by introducing a set of window events to provide a two-way communication protocol between Ethereum libraries and injected scripts provided by browser extensions thus enabling users to select their wallet of choice.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;realizing-the-potential&quot;&gt;Realizing the Potential&lt;/h2&gt;
&lt;p&gt;Upon learning about this standard, I immediately saw an opportunity. At Blockaid, we pride ourselves on partnerships with major players like &lt;a href=&quot;https://www.blockaid.io/blog/blockaid-metamask-securing-web3-users-while-preserving-privacy&quot;&gt;Metamask&lt;/a&gt;, &lt;a href=&quot;https://www.blockaid.io/blog/coinbase-wallet-powered-by-blockaid&quot;&gt;Coinbase Wallet&lt;/a&gt;, &lt;a href=&quot;https://www.blockaid.io/blog/rainbow-wallet-mobile-app-and-browser-extension-powered-by-blockaid&quot;&gt;Rainbow&lt;/a&gt;, and &lt;a href=&quot;https://www.blockaid.io/blog/zerion-phishing-defense-powered-by-blockaid&quot;&gt;Zerion&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My idea? Use EIP-6963 to provide social proof to our website visitors.&lt;/p&gt;
&lt;p&gt;By highlighting that the wallets they use are likely already compatible with Blockaid, we can build trust and assure them of their choice of us as their security provider—the same idea that is powering our messaging around the Blockaid Network.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;blockaid-network-protection-pitch.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;the-integration-process&quot;&gt;The Integration Process&lt;/h2&gt;
&lt;p&gt;Integrating support for this standard was surprisingly easy. The injected providers that support EIP-6963 publish a window event with a simple interface providing necessary information, including the provider’s name and icon.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;eip6963-provider-interface-code.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;With this interface, the integration is very straightforward - all you need to do is to listen for window events that contain a provider announcement, make sure that the announced provider is one of our supported providers (list of our customers that integrated EIP-6963 into their extension), and set the state of the banner:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#616E88&quot;&gt;// Listener for wallet provider announcements&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;eip6963:announceProvider&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; icon&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; event&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;detail&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;Object&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;keys&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;supportedProviders&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;      setProvider&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;supportedProviders&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;])&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the code listens for provider announcements and updates our website to reflect the connected provider, offering a seamless user experience. And the result? Stunning.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img src=&quot;https://pbs.twimg.com/media/GOFY8oJXIAAcPKE?format=jpg&amp;#x26;name=large&quot; alt=&quot;Image&quot;&gt;&lt;/figure&gt;
&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;EIP-6963 is more than just a technical improvement—it’s a trust signal, a marketing asset, and a way to reduce friction for users navigating the Web3 ecosystem.&lt;/p&gt;
&lt;p&gt;By showcasing compatibility with this standard, we’re not just improving UX; we’re reinforcing security, credibility, and seamless integration with the wallets users already trust.&lt;/p&gt;
&lt;p&gt;For any crypto project looking to boost adoption, leveraging new standards like EIP-6963 isn’t just a technical decision—it’s a strategic one.&lt;/p&gt;
&lt;p&gt;And in an industry where trust is everything, small details like these can make a big difference.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A special mention to&lt;/em&gt; &lt;a href=&quot;https://github.com/MetaMask/vite-react-ts-eip-6963&quot;&gt;&lt;em&gt;this Metamask repository&lt;/em&gt;&lt;/a&gt; &lt;em&gt;for being an invaluable resource during this process.&lt;/em&gt;&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.DKwGzbnD_28OX6x.jpeg" medium="image"/></item><item><title>Putting an end to 3D printed Halloween models</title><link>https://posts.oztamir.com/putting-an-end-to-3d-printed-halloween-models/</link><guid isPermaLink="true">https://posts.oztamir.com/putting-an-end-to-3d-printed-halloween-models/</guid><description>I was tired of seeing 3D Printed Halloween models - so I created a browser extension to fix it.</description><pubDate>Sat, 05 Oct 2024 14:09:25 GMT</pubDate><content:encoded>&lt;p&gt;Ah, October. It’s the time of year when every 3D printing model site goes Halloween-crazy. If you’ve been browsing &lt;a href=&quot;https://www.printables.com/model&quot;&gt;Printables&lt;/a&gt;, &lt;a href=&quot;https://www.thingiverse.com/&quot;&gt;Thingiverse&lt;/a&gt;, or &lt;a href=&quot;https://makerworld.com/en/3d-models&quot;&gt;Makerworld&lt;/a&gt; lately, you know what I’m talking about. Skeletons, witches, pumpkins—it’s as if every other model has been dipped in spooky sauce.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;printables-halloween-models-grid.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Now, don’t get me wrong—Halloween’s fun and all. But not when you’re just looking for cool new prints, and all you get is a flood of bats, cobwebs, and the same jack-o’-lantern model in 30 different styles. Since I’m not exactly in the Halloween spirit (being from a non-celebrating country and all), this overload felt more like spam.&lt;/p&gt;
&lt;p&gt;So, instead of just scrolling past in frustration, I figured, why not do something about it? Thus, I built a browser extension that filters out all the Halloween-themed content and restores balance to my 3D model browsing experience. No more spooky clutter on my screen.&lt;/p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Every October, these 3D printing sites look like they’ve been taken over by Halloween.&lt;/p&gt;
&lt;p&gt;If you’re actually hunting for new prints, it becomes this endless scavenger hunt to sift through all the spooky clutter. Skeleton hands, pumpkin lanterns, ghost cookies—every search query ends up returning more of the same. It wasn’t long before I found myself spending more time filtering out seasonal stuff in my head than actually finding useful models.&lt;/p&gt;
&lt;p&gt;Annoyed by this, I decided to take matters into my own hands and filter it all out—programmatically.&lt;/p&gt;
&lt;h2 id=&quot;the-solution-building-a-halloween-filter-browser-extension&quot;&gt;The Solution: Building a Halloween-Filter Browser Extension&lt;/h2&gt;
&lt;p&gt;So, how do you block out Halloween models without blocking the entire site? The solution was to create a &lt;a href=&quot;https://github.com/OzTamir/FilterHalloweenModels/&quot;&gt;browser extension&lt;/a&gt; that hides or removes items based on specific keywords, like “pumpkin,” “witch,” “ghost,” “skeleton,” etc.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;extension-icon-halloween-filter.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;The icon ChatGPT helped me create&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This extension works by running a background script that detects when a user is visiting one of the aforementioned 3D printing sites. For those unfamiliar, a background script in browser extensions runs in the background, waiting for specific events or actions to occur.&lt;/p&gt;
&lt;p&gt;In my case, the script listens for a page load event (i.e., when you load up Printables or Thingiverse), then executes the filtering logic to hide any Halloween-related models.&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;chrome&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;tabs&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;onUpdated&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;addListener&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;tabId&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; changeInfo&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tab&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;changeInfo&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ===&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;complete&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;tab&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;printables.com&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;      chrome&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;scripting&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;executeScript&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        target&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; tabId&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tabId&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        files&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;halloweenRemovers/printables.js&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;tab&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;thingiverse.com&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;      chrome&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;scripting&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;executeScript&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        target&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; tabId&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tabId&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        files&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;halloweenRemovers/thingyverse.js&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;tab&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;makerworld.com&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;      chrome&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;scripting&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;executeScript&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        target&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; tabId&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; tabId&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;        files&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;halloweenRemovers/makerworld.js&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the page is fully loaded, the extension injects a specific script depending on the site you’re visiting. You might wonder why we need to inject a script at all—can’t the background script handle everything? Well, not quite.&lt;/p&gt;
&lt;p&gt;In this case, the injected script needs to manipulate the HTML of the webpage directly. This means it needs to run in the same JavaScript context as the website to access and modify the DOM.&lt;/p&gt;
&lt;p&gt;Imagine the webpage is a sandbox where JavaScript is running. To change things inside that sandbox (like hiding a Halloween model), the script itself must run within the sandbox. That’s where the injected script comes in—it gets dropped into the website’s sandbox and gets full access to modify the page as needed.&lt;/p&gt;
&lt;h3 id=&quot;the-filtering-logic-removing-halloween-models-in-action&quot;&gt;The Filtering Logic: Removing Halloween Models in Action&lt;/h3&gt;
&lt;p&gt;Once the extension detects that you’ve loaded one of the supported 3D printing sites, it injects a script tailored to the site’s specific layout.&lt;/p&gt;
&lt;p&gt;The filtering logic works by scanning the page’s HTML and comparing each model’s details against a list of Halloween-related keywords (like “pumpkin,” “witch,” and “ghost”).&lt;/p&gt;
&lt;p&gt;If a match is found, the offending model gets swiftly removed from the page. Before it does, though, it loads the list of words deemed to be related to Halloween:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; halloweenWords&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; []&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#616E88&quot;&gt;// Function to load Halloween words from chrome.storage&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; loadHalloweenWords&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;  chrome&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;storage&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;local&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;([&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;halloweenWords&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;result&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;chrome&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;runtime&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;lastError&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;      console&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Error loading Halloween words:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; chrome&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;runtime&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;lastError&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;      return;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    halloweenWords&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;halloweenWords&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; []&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;    console&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Loaded Halloween words:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; halloweenWords&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;halloweenWords&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;length &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#B48EAD&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;      console&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;warn&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;No Halloween words loaded. Check if words are being saved correctly.&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;      )&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;    removeArticles&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have the words, we can iterate over the items (in Printable’s’ HTML code, they appear inside &lt;code&gt;&amp;#x3C;article&gt;&lt;/code&gt; tags - which is why the code refers to articles):&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; removeArticles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#616E88&quot;&gt;  // Updated selector to match the correct class name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; articles&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; document&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;article.card.svelte-j1lj8e&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;  console&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Found &lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;articles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt; articles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;  articles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;forEach&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;article&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; index&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; link&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; article&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;a[href^=&apos;/model/&apos;]&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;link&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; href&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; link&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;getAttribute&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;href&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;toLowerCase&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;halloweenWords&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;some&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;word&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; href&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;word&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;))) &lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;        console&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;warn&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;Removed article:&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;          title&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; article&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;h2&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;?.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;textContent&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;No title&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;          link&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; href&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;        }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;        article&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;remove&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;  }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, since these sites are loading more models on the go, I’ve also added a &lt;code&gt;MutationObserver&lt;/code&gt; that will re-run the filtering logic whenever the &lt;code&gt;body&lt;/code&gt; changes:&lt;/p&gt;
&lt;pre class=&quot;astro-code nord&quot; style=&quot;background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt; observer&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; MutationObserver&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;  console&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A3BE8C&quot;&gt;MutationObserver triggered, running removeArticles&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;  removeArticles&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;observer&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt;observe&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; childList&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#88C0D0&quot;&gt; subtree&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#ECEFF4&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#D8DEE9FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#81A1C1&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;keeping-things-dynamic-updating-the-keyword-list&quot;&gt;Keeping Things Dynamic: Updating the Keyword List&lt;/h2&gt;
&lt;p&gt;One of the nice parts of this extension is that the list of banned keywords is editable by the user. This way, it’s not just limited to Halloween. You can add keywords to filter out Christmas models in December or Valentine’s Day models in February.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;extension-settings-keyword-list.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The extension stores the user’s blacklist in &lt;code&gt;chrome.storage.local&lt;/code&gt;, meaning it persists even after the browser is closed. This makes the extension highly adaptable for any kind of seasonal content—just add the words you want to avoid, and it’ll clean up your feed accordingly.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;While 3D printing model sites haven’t (yet) introduced features to filter out unwanted seasonal content, this extension fills that gap. With a dynamic, customizable keyword list, I can now look for new models to print - without being bombarded by pumpkins and witches in October.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;printables-art-models-no-halloween.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Same page - with FHM extension enabled&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;Same page - with FHM extension enabled&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The final extension is available on GitHub &lt;a href=&quot;https://github.com/OzTamir/FilterHalloweenModels/&quot;&gt;here&lt;/a&gt;. Installation instructions are provided in the README file.&lt;/p&gt;
&lt;p&gt;Whether you’re as tired as I am of seasonal model spam or simply want more control over what you see, this extension gives you the power to customize your 3D printing feed to your heart’s content.&lt;/p&gt;
&lt;p&gt;It’s not perfect, but it sure makes for a less spooky browsing experience. 🎃&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.De3-mgpy_1Jxudg.jpeg" medium="image"/><category>3d-printing</category><category>Browser extension</category></item><item><title>Broke: An Open Source Alternative to Brick</title><link>https://posts.oztamir.com/broke-an-open-source-alternative-to-brick/</link><guid isPermaLink="true">https://posts.oztamir.com/broke-an-open-source-alternative-to-brick/</guid><description>How I used AI to write Broke: an NFC tag that blocks distractions, inspired by Brick.</description><pubDate>Sat, 24 Aug 2024 16:58:16 GMT</pubDate><content:encoded>&lt;p&gt;I recently came across a nifty little project called &lt;a href=&quot;https://getbrick.app&quot;&gt;Brick&lt;/a&gt;. Brick is a simple yet powerful product - it’s a physical tag that allows you (using a dedicated iOS app) to block certain apps an activities on your phone, with unlocking only possible by using the physical tag that was used to lock them.&lt;/p&gt;
&lt;p&gt;The idea behind this workflow is to enable you to create a physical barrier that keeps you from engaging with distractions on your phone - for example, you can use the Brick to block all non-vital apps on your phone before going to the coffee shop to get some work done. Once you’ve left the tag at home, you cannot be tempted into disabling the block - as you simply cannot disable it until you get back home and has the tag available again.&lt;/p&gt;
&lt;p&gt;When I first discovered Brick, I was intrigued by its concept - However, as much as I liked the idea, I felt that Brick had more features than I personally needed—and I wasn’t keen on paying 50$ for something I felt I can hack together in a weekend. So instead of paying, I decided to do just that - build my own alternative, aptly named &lt;strong&gt;Broke&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a good opportunity to give a shoutout to the Brick team - if this concept seems valuable to you, I highly recommend checking them out - their app has a lot of features I didn’t bother implementing, and the experience they provide is much more production ready.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;from-concept-to-reality-creating-broke&quot;&gt;From Concept to Reality: Creating Broke&lt;/h2&gt;
&lt;p&gt;To get started, I ordered some affordable NFC tags from AliExpress. I initially ordered ones that matched the ones used by Brick, but later I’ve decided to make my app stand on it’s own completely, and wrote the code to support any NFC tag supported by iOS.&lt;/p&gt;
&lt;p&gt;I also spent some time creating a 3D Printed case, to allow for a sleek experience. After a few iterations, I built a case that had magnet holes (to allow leaving it on the fridge) and a keychain hole, to allow carrying it around.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;3d-printed-case-prototypes.jpeg&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;It took multiple tries, but I&amp;#x27;m really happy with the end product&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;It took multiple tries, but I’m really happy with the end product&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;writing-the-ios-app-with-a-little-help&quot;&gt;Writing the iOS app, with a little help&lt;/h2&gt;
&lt;p&gt;Once I had a physical tag and a case, it was time to write the app that would implement the real functionality. There was only a tiny problem - I had zero experience with iOS development.&lt;/p&gt;
&lt;p&gt;However, I did had a good idea of how I wanted the app to work. I knew that it would need to support reading and writing NFC tags (to allow the interaction of scanning a tag to trigger an action), and that it would have to interface with the Screen Time APIs that iOS exposed to allow developers to limit interaction with other apps.&lt;/p&gt;
&lt;p&gt;Armed with this outline, I embarked on a quest that I suspect would be my first foray into the future of programming, and how it’s gonna look like for all code projects in the not so distant future - pair programming with AI. I used Claude, my LLM of choice, to start building the user interface.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;claude-writing-swiftui-code.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Working with Claude was great - I was quickly able to put a basic PoC that allowed me to demonstrate that the app worked.&lt;/p&gt;
&lt;figure class=&quot;image-card has-caption&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;broke-app-poc-screens.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;The Proof of concept design&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;But the real game changer was when I followed a recommendation from a fellow developer on Twitter - which prompted me to start using &lt;a href=&quot;https://www.cursor.com/&quot;&gt;Cursor&lt;/a&gt; - a VS Code-based AI IDE allowed me to “talk” with my codebase (using embeddings), which made understanding and navigating the code so much easier. With Cursor, I was able to quickly transform my basic proof of concept into a functional, polished app—something that actually feels like it could belong on the App Store.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;xcode-broke-app-with-claude.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;final-touches&quot;&gt;Final Touches&lt;/h2&gt;
&lt;p&gt;Once I was done with the core functionality, I took some time to design something that would look presentable. I never designed for iOS before, but after playing in Figma for a while I got to result I was OK with. If this was a full time product, I’d probably give it some more refinement - but for a pet-project, this was good enough.&lt;/p&gt;
&lt;figure class=&quot;image-card&quot;&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;broke-icon-color-variants.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This is how the final experience looks like:&lt;/p&gt;
&lt;figure class=&quot;video-card relative&quot;&gt;&lt;video src=&quot;iphone-nfc-scan-ready.mp4&quot; poster=&quot;iphone-nfc-scan-ready-poster.png&quot; controls preload=&quot;metadata&quot; playsinline class=&quot;block h-auto w-full max-w-full&quot;&gt;&lt;/video&gt;&lt;/figure&gt;
&lt;h2 id=&quot;sharing-broke&quot;&gt;Sharing Broke&lt;/h2&gt;
&lt;p&gt;I want to share my project with the world, but releasing a free alternative to Brick will be what people in bird culture refer to as a dick move.&lt;/p&gt;
&lt;p&gt;Therefor, I won’t be releasing Broke as an app anyone can download, nor will I be letting people buy Broke tags from me - Instead, I’ve open-sourced the project, to make it available as an option for technical folks like me, or for anyone interested in building upon my work.&lt;/p&gt;
&lt;p&gt;The code for the app is available on &lt;a href=&quot;https://github.com/OzTamir/broke/tree/main?tab=readme-ov-file&quot;&gt;Github&lt;/a&gt;, while the design for the tags is available on &lt;a href=&quot;https://www.printables.com/model/983618-broke-tag-nfc-tag-cover-with-keychain-and-magnet-h&quot;&gt;Printables&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now that the project is complete, I’m thrilled with the outcome. I have created the product that I wanted to use, and I did so by using my own code, CAD, and 3D printing skills. Here’s how it turned out:&lt;/p&gt;
&lt;p&gt;Reflecting upon this project, I must say that It never ceases to amaze me how much power a modern-day hobbyists have on their hands - I was able to create this thing from scratch over a weekend! Amazing. Also, I feel like this experience is my answer to folks saying that programming will be gone in a couple of years - I don’t think that programmers will be replaced by AI. I think that the programmers who will fail to embrace the possibilities unlocked by AI will be left behind, but those of us who will - are about to unlock abilities beyond their wildest imagination.&lt;/p&gt;
&lt;p&gt;Exciting times!&lt;/p&gt;</content:encoded><dc:creator>Oz Tamir</dc:creator><media:content url="https://posts.oztamir.com/_astro/featured.DTaUJ-NS_Z2dy47s.jpeg" medium="image"/><category>ios</category><category>ai</category><category>Projects</category><category>Programming</category><category>nfc</category></item></channel></rss>