<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Achiya Automation Blog]]></title><description><![CDATA[WhatsApp bots, business automation, and AI agents for small businesses. Built 50+ automation projects with n8n, WAHA, and the official WhatsApp Cloud API. Based in Israel, serving clients worldwide.]]></description><link>https://achiya-automation.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 04 Sep 2026 19:20:32 GMT</lastBuildDate><atom:link href="https://achiya-automation.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Chatwoot pricing line nobody reads: your conversation history has an expiry date]]></title><description><![CDATA[I run Chatwoot in production for eight client accounts — 20 inboxes, 34 seats, about 350,000 messages since November 2025. Recently I sat down to work out what the same setup would cost on Chatwoot Cl]]></description><link>https://achiya-automation.hashnode.dev/the-chatwoot-pricing-line-nobody-reads-your-conversation-history-has-an-expiry-date</link><guid isPermaLink="true">https://achiya-automation.hashnode.dev/the-chatwoot-pricing-line-nobody-reads-your-conversation-history-has-an-expiry-date</guid><dc:creator><![CDATA[אחיה כהן]]></dc:creator><pubDate>Wed, 02 Sep 2026 20:53:29 GMT</pubDate><content:encoded><![CDATA[<p>I run Chatwoot in production for eight client accounts — 20 inboxes, 34 seats, about 350,000 messages since November 2025. Recently I sat down to work out what the same setup would cost on Chatwoot Cloud, expecting the answer to be a simple multiplication: seats times tier price.</p>
<p>It wasn't. The number that decides the tier turned out to have nothing to do with the monthly price.</p>
<h2>Every hosted tier has a retention cap</h2>
<p>Chatwoot Cloud keeps your conversation history for a bounded window, and the window is set by the tier:</p>
<ul>
<li><p>Free tier: 30 days</p>
</li>
<li><p>Entry paid tier: 6 months</p>
</li>
<li><p>Mid tier: 1 year</p>
</li>
<li><p>Top tier: 2 years</p>
</li>
</ul>
<p>That is not a soft limit or an archive. Older conversations are removed.</p>
<p>My oldest message is from 25 November 2025. As I write this that is 282 days of history. On the entry paid tier, roughly three months of client conversations would already be gone.</p>
<h2>Why this is an architecture decision, not a billing detail</h2>
<p>A support inbox is not a chat app. It is a record.</p>
<p>The questions that make it valuable are all backwards-looking. What did we quote them in March? Did we ever tell this customer the migration was included? Which agent handled the escalation, and what did they promise? Every one of those needs history that outlives the incident.</p>
<p>Once you frame it that way, the retention cap stops being a line in a pricing table and becomes a constraint on what the system is for. A tool that forgets six months back cannot answer "what did we agree in the spring."</p>
<p>Self-hosted has no retention cap at all. Your history lasts as long as your disk does — and disk is cheap. My whole Postgres database, 350,000 messages in, is under a gigabyte.</p>
<h2>What it costs to keep it</h2>
<p>The natural follow-up is what self-hosting actually consumes, since "free licence" and "free" are not the same thing. Measured on my production install, steady state: <code>chatwoot-rails-1</code> 827 MiB, <code>chatwoot-postgres-1</code> 595 MiB, <code>chatwoot-sidekiq-1</code> 368 MiB, <code>chatwoot-redis-1</code> 7 MiB — <strong>1.80 GiB in total.</strong></p>
<p>Under two gigabytes of RAM for 20 inboxes. A modest VPS runs it comfortably, and — the part that matters for the comparison — <strong>that footprint tracks message volume, not seat count.</strong> Adding an agent costs nothing. On any hosted plan, each seat is a recurring line item.</p>
<p>I put the full tier-by-tier comparison, including where the crossover sits at different team sizes, in a longer write-up: <a href="https://achiya-automation.com/en/blog/chatwoot-pricing/">what Chatwoot actually costs to run</a>.</p>
<h2>The honest counterweight</h2>
<p>Self-hosting is not free, it is differently expensive. Over nine months I have worked through Postgres connection-pool exhaustion under load, a Redis eviction policy that silently dropped background jobs, a log rotation config that corrupted an access log badly enough to break my own monitoring, and an upgrade that reverted files I had mounted into the container.</p>
<p>None of those were Chatwoot's fault. All of them were the price of the zero-cost licence, paid in evenings.</p>
<p>If nobody on your team is comfortable on a server at 11pm, hosted is genuinely cheaper for you, because the alternative is not "a server" — it is "a server plus a person."</p>
<p>But do read the retention row before you pick the tier. It is the constraint that will actually bite, and it is the one nobody quotes.</p>
<p><em>Figures measured on a production install, September 2026. I build WhatsApp bots and business automation at</em> <a href="https://achiya-automation.com/en/"><em>Achiya Automation</em></a><em>, and I run self-hosted Chatwoot for clients.</em></p>
]]></content:encoded></item><item><title><![CDATA[My AI agent saved the first paragraph and the last. It dropped 41 in between.]]></title><description><![CDATA[> **Originally published on dev.to:** [my-ai-agent-saved-the-first-paragraph-and-the-last-it-dropped-41-in-between-app](https://dev.to/achiya-automation/my-ai-agent-saved-the-first-paragraph-and-the-l]]></description><link>https://achiya-automation.hashnode.dev/my-ai-agent-saved-the-first-paragraph-and-the-last-it-dropped-41-in-between</link><guid isPermaLink="true">https://achiya-automation.hashnode.dev/my-ai-agent-saved-the-first-paragraph-and-the-last-it-dropped-41-in-between</guid><dc:creator><![CDATA[אחיה כהן]]></dc:creator><pubDate>Tue, 12 May 2026 15:51:52 GMT</pubDate><content:encoded><![CDATA[<p>&gt; **Originally published on dev.to:** [my-ai-agent-saved-the-first-paragraph-and-the-last-it-dropped-41-in-between-app](<a href="https://dev.to/achiya-automation/my-ai-agent-saved-the-first-paragraph-and-the-last-it-dropped-41-in-between-app">https://dev.to/achiya-automation/my-ai-agent-saved-the-first-paragraph-and-the-last-it-dropped-41-in-between-app</a>)</p>
<p>I asked an AI agent to cross-post a 7,000-character article from dev.to to Hashnode.</p>
<p>The Submit click succeeded. Hashnode returned a draft URL. I clicked through.</p>
<p>The draft had **446 characters**: the first paragraph, then 41 empty paragraphs, then the last paragraph.</p>
<p>This is a postmortem of how I got there, why my first three diagnoses were wrong, and what fixed it. If you're shipping any kind of browser automation that touches modern rich-text editors, this one is worth the read.</p>
<p>## The setup</p>
<p>[Safari MCP](<a href="https://github.com/achiya-automation/safari-mcp">https://github.com/achiya-automation/safari-mcp</a>) is the macOS-native browser automation tool I maintain. One of the things it has to do is fill rich-text editors — Quill, ProseMirror, Lexical, the React-controlled stuff Featured.com uses, and a dozen variations.</p>
<p>For the cross-posting flow specifically, the agent does this:</p>
<p>- Opens the Hashnode "new draft" page in my real Safari (already signed in).<br />- Drops a 7,000-character markdown body into the editor.<br />- Clicks Publish.</p>
<p>That's it. It worked for years on dev.to, Medium, X, LinkedIn (after their 2026 Quill migration), Featured.com. Hashnode was supposed to be the easy one — they sell themselves as "developer-friendly".</p>
<p>## The symptom</p>
<p>After Submit, the draft saved with the first paragraph intact, then 41 empty paragraphs, then the last paragraph intact.</p>
<p>Total saved: 446 chars. Total sent: 6,808 chars. **The middle 94% was silently dropped.**</p>
<p>The agent had no error. The fill call returned successfully. The Submit click returned successfully. Hashnode's own draft view showed the broken structure as if it were intentional.</p>
<p>## First diagnosis (wrong): paste race condition</p>
<p>My first guess was a paste-event timing issue. I'd recently fixed a similar bug on X.com where the synthetic `ClipboardEvent('paste')` was racing with their React `useEffect` cycle. The fix had been an explicit `execCommand('delete')` before the paste.</p>
<p>I tried the same thing here. No change.</p>
<p>I added a `safari_verify_state` call between fill and submit. The verifier confirmed the editor's `.textContent` matched what I sent — at the moment I checked. But by the time Submit fired ~100ms later, the editor state had reverted.</p>
<p>So whatever was eating the middle paragraphs, it was doing it after the fill returned. The agent's "success" signal was lying.</p>
<p>## Second diagnosis (wrong): markdown auto-conversion</p>
<p>Hashnode's editor does auto-format certain characters at the start of a line: `&gt;` becomes blockquote, `**` becomes bold, `[` opens a link helper, `#` becomes heading.</p>
<p>I noticed the body had several paragraphs starting with these characters. So I theorized: the editor was rejecting paragraphs whose first characters tripped auto-format prompts, leaving them empty until the user manually accepted.</p>
<p>Fix: I escaped the leading characters with a zero-width space. Re-ran. Result: **still 446 chars saved.**</p>
<p>So that wasn't it either.</p>
<p>## Third diagnosis (wrong): React reconciliation order</p>
<p>Hashnode's editor wraps ProseMirror in a React component. I suspected that the multiple `beforeinput` events I was dispatching were getting batched and only the first + last applied.</p>
<p>I switched from `beforeinput` to `composing` text events with intermediate `setTimeout(0)` calls to give React's render cycle a chance to flush.</p>
<p>Still 446 chars.</p>
<p>At this point I was four hours in and getting irrationally angry at a contenteditable div.</p>
<p>## The real bug</p>
<p>I read the Safari MCP fill pipeline. For ProseMirror editors, it walks the DOM looking for `.pmViewDesc`, if found dispatches via view.dispatch. If not found, walks React Fiber for memoizedProps.view. If still not found, **falls through to char-by-char `beforeinput` + `execCommand('insertText')` per line.**</p>
<p>Path 4 is the fallback for editors that don't expose ProseMirror's internals. It's worked everywhere I'd tested it. Including Hashnode in earlier dev.</p>
<p>But it has one assumption baked in: **that the editor will accept the text it's told to insert.** If the editor rejects an insert silently — for any reason — the fill pipeline never finds out. The function returns success. The DOM has empty paragraphs.</p>
<p>Hashnode's ProseMirror configuration has an "input rules" plugin that runs on every paragraph start. The plugin's job is to handle markdown shortcuts. But its implementation aborts the insert if the matched text doesn't form a valid command — and just doesn't insert anything.</p>
<p>So a blockquote-prefixed line doesn't become a blockquote. It also doesn't become a regular paragraph. It becomes nothing.</p>
<p>The fill pipeline is char-by-char per line. It walks down, fires beforeinput, fires execCommand. The input rule fires on each markdown-shortcut character, kills the line silently. Pipeline moves to next line. Same thing.</p>
<p>Only paragraphs whose first character doesn't trip a rule survive. In my article, that was paragraph 1 and the final paragraph.</p>
<p>## The fix</p>
<p>The fix is straightforward once you see it: after the char-by-char fill, verify the editor's textContent length. If less than 60% of what you sent landed, clear the editor via DOM removal and re-fill via `execCommand('insertHTML', false, paragraphWrappedHtml)`. `insertHTML` bypasses the input-rules plugin because the rules only fire on character-level input events. A bulk HTML insert is treated as a paste and goes through a different code path — one that doesn't run the markdown-shortcut interceptor.</p>
<p>Verify-after-fill is the part that took me too long to add. Trust the framework to tell you what happened, not the call that just returned.</p>
<p>This shipped in [v2.10.4](<a href="https://github.com/achiya-automation/safari-mcp/releases/tag/v2.10.4">https://github.com/achiya-automation/safari-mcp/releases/tag/v2.10.4</a>) of Safari MCP.</p>
<p>## The lesson</p>
<p>The deeper issue isn't ProseMirror. It's the assumption that **a successful tool call means the action succeeded.**</p>
<p>Browser automation has a recurring failure mode: the framework keeps its own state separately from the DOM, and the framework's state is what gets submitted on form post. Your synthetic events change one or the other, sometimes both, sometimes neither. The DOM looks right. The submit ships the wrong thing.</p>
<p>I added a tool called `safari_verify_state` in v2.10.0 specifically for this. It checks framework state (ProseMirror view, Lexical editor state, React `_valueTracker` desync, Closure component values) and returns whether the framework agrees with the DOM. The Hashnode ProseMirror case is now a built-in check.</p>
<p>If you're building any agent that touches a serious editor — Quill, ProseMirror, Lexical, Slate, Tiptap — assume the fill happened, then **verify what landed before you click Submit.** The 5 ms it takes is the cheapest insurance you'll buy this year.</p>
<p>## Postscript</p>
<p>I cross-posted this article using the fixed code path. It saved as 41 intact paragraphs.</p>
<p>The agent didn't notice anything was different. That's the point.</p>
<p>---</p>
<p>[Safari MCP](<a href="https://github.com/achiya-automation/safari-mcp">https://github.com/achiya-automation/safari-mcp</a>) is open source (MIT) and runs on macOS. It's used by Claude Code, Cursor, and other MCP clients to drive a real, logged-in Safari instead of spinning up a headless Chromium.</p>
]]></content:encoded></item><item><title><![CDATA[An AI agent overwrote two of my browser tabs. The fix took three releases.]]></title><description><![CDATA[Cross-posted from dev.to — original published 2026-05-07.
I was eating dinner when my AI agent ate my tabs.
I had Safari open with a Chatwoot Meta dashboard in one tab and an n8n executions view in an]]></description><link>https://achiya-automation.hashnode.dev/an-ai-agent-overwrote-two-of-my-browser-tabs-the-fix-took-three-releases</link><guid isPermaLink="true">https://achiya-automation.hashnode.dev/an-ai-agent-overwrote-two-of-my-browser-tabs-the-fix-took-three-releases</guid><category><![CDATA[AI]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[אחיה כהן]]></dc:creator><pubDate>Fri, 08 May 2026 15:23:45 GMT</pubDate><content:encoded><![CDATA[<p>Cross-posted from <a href="https://dev.to/achiya-automation/an-ai-agent-overwrote-two-of-my-browser-tabs-the-fix-took-three-releases-l2l">dev.to</a> — original published 2026-05-07.</p>
<p>I was eating dinner when my AI agent ate my tabs.</p>
<p>I had Safari open with a Chatwoot Meta dashboard in one tab and an n8n executions view in another — both with unsaved state, both in the middle of real work. In a third tab, my own tab, my agent was supposed to be testing a new feature in the MCP server I maintain (<a href="https://github.com/achiya-automation/safari-mcp">Safari MCP</a>).</p>
<p>I came back to the laptop and both real-work tabs had been navigated to URLs the agent picked.The Chatwoot tab was now showing some test page. The n8n tab was on a Reddit comment thread the agent had been debugging an unrelated module against.</p>
<p>The agent hadn't gone rogue. The MCP server had a state-tracking bug — and instead of failing loudly, it had silently fallen back to "use whatever tab the user is on."</p>
<p>This is a postmortem. The fix took three releases — v2.10.0, v2.10.1, and v2.10.3 — and the iteration is the interesting part.</p>
<h2>The shape of the bug</h2>
<p>Safari MCP exposes a safari_new_tab(url) tool. Internally, it tracks "the tab MCP owns" via a tab index (_activeTabIndex) — Safari's positional handle — and a DOM marker (window.__mcpTabMarker) — injected JS that lets future calls verify "yes, this is still our tab."</p>
<p>Every subsequent safari_navigate, safari_click, safari_fill etc. resolves "where to act" by: (1) check marker in current tab, (2) else check _activeTabIndex still valid, (3) else fall back to "front document of front window".</p>
<p>That last branch is the catastrophe. "Front document of front window" is, by definition, whatever the user is looking at right now.</p>
<p>So why did the fallback fire? Three different reasons, across three releases.</p>
<h2>v2.10.0 — the original failure mode</h2>
<p>The original safari_new_tab(url) did exactly what its name said: open a new tab and navigate it to url in one call. If navigate(idx, url) <em>fails to load</em> — file:// blocked by Safari, network error, an app:// scheme that Safari doesn't understand — the new tab stays at about:blank. The marker injection runs, but then the next user-driven navigation in any tab can wipe it. By the time the next safari_navigate arrives, our marker check fails. The "front document" fallback fires. We navigate the user's current tab.</p>
<p>I shipped this. I tested it on a clean Safari with one window. I never hit the bug because in clean state, the user's tab <em>is</em> my tab.</p>
<h2>v2.10.1 — the grace window (almost-fix)</h2>
<p>The first fix was a NEW_TAB_GRACE_MS = 30000 window. For 30 seconds after safari_new_tab, ANY mutating operation that <em>would</em> fall back to the user's tab now throws a clear error. Plus a fix for the marker wipe — safari_navigate now re-injects window.__mcpTabMarker after every successful navigation.</p>
<p>This passed all my tests. It also worked correctly for ~95% of real sessions.</p>
<p>The 5% it missed: <strong>sessions longer than 30 seconds where the tab-ghost recovery path nullified _activeTabIndex mid-session.</strong></p>
<h2>v2.10.3 — the permanent guard</h2>
<p>runJS (the workhorse for every JS-driven tool) has a tab-ghost recovery path. If a JavaScript evaluate fails because the tab Safari thinks is at index N has been closed/replaced, runJS nullifies _activeTabIndex so the next call resolves cleanly. The intent: avoid using a stale index after Safari shuffles tabs.</p>
<p>The unintended consequence: 30+ minutes into a session, after a routine ghost-recovery, _activeTabIndex is null. The grace window from v2.10.1 has long expired. The marker check fails. Fallback fires. User's current tab gets clobbered.</p>
<p>The bug pattern: <strong>a "safe" recovery path created the exact failure mode the grace window was designed to prevent.</strong></p>
<p>The permanent fix is a sticky session-scoped flag (_hasOwnedTab) that's set the first time safari_new_tab succeeds, and never resets for the lifetime of the MCP process. Four entry points — _assertNotFallingBackToUserTab (used by navigate and navigateAndRead), runJS's tab-ghost fallback path, and runJSLarge — all call this assertion before falling back to the user's current tab. If the assertion throws, the agent gets a clear error pointing back at safari_new_tab. The user's tab is untouched.</p>
<h2>What I'd take away if I were writing my own MCP server</h2>
<p><strong>1. The fallback you don't notice is the fallback that bites.</strong> "Use the user's current tab" looks like a reasonable degraded mode in isolation. In context — an autonomous agent acting on the user's real, logged-in browser — it's the worst possible default. The fix wasn't "make the fallback work better." It was "the fallback should not exist in this branch of the state machine."</p>
<p><strong>2. State-tracking bugs aren't subtle. They're catastrophic.</strong> A misidentified tab is a misidentified action. The class of bug here — <em>I think I'm acting on X but I'm actually acting on Y</em> — is the same class as a deployment script targeting prod instead of staging, or a Git rebase rewriting the wrong branch. There's no "minor version" of this bug.</p>
<p><strong>3. "Sticky" flags beat "windowed" flags for invariants you actually need.</strong> The v2.10.1 grace window was time-bounded. But sessions are unbounded. <em>Anything that can happen during the session can happen after the grace window expires.</em> If the property "MCP has owned a tab in this session" is the actual thing protecting the user, that property must hold for the whole session — not 30 seconds.<strong>4. Tests on clean state miss the bugs that matter.</strong> I tested v2.10.0 on a fresh Safari with no other tabs. The user-tab-clobber bug is <em>invisible</em> in that environment because the user's tab and MCP's tab are the same tab. Real users have eight tabs open and were just clicking around in tab six. If your tool drives a user's real browser, your test environment must have <em>unrelated, in-progress tabs</em>.</p>
<p><strong>5. Errors are a feature.</strong> The replacement for "silent fallback to user tab" is a thrown error with a remediation message: <em>"Call safari_new_tab to re-establish."</em> That error is <em>better than the original happy path</em> — because the original happy path was sometimes a disaster. A loud, fixable error is always better than a quiet, irreversible mistake.</p>
<h2>The diff that mattered</h2>
<p>The load-bearing change is one line of state plus a guarded fallback function: a sticky boolean <code>_hasOwnedTab</code> that flips true on the first successful safari_new_tab and gates the fallback to the user's current tab. That's the load-bearing part. Everything else in v2.10.3 is plumbing.If you're building an MCP server (or any tool that drives a user's real browser/editor/database), the question I'd ask in code review is: <em>"What's the worst thing this fallback can do, and does the fallback's existence buy enough to be worth that worst case?"</em> For tab fallback in Safari MCP, the answer was: <strong>no, it doesn't.</strong></p>
<p><strong>Source:</strong> <a href="https://github.com/achiya-automation/safari-mcp">github.com/achiya-automation/safari-mcp</a><br /><strong>Install:</strong> <code>npx safari-mcp</code><br /><strong>Releases discussed:</strong> v2.10.0, v2.10.1, v2.10.3</p>
<p><em>Have you shipped a state-tracking bug that ate user data? What was the failure mode, and what flag/invariant ended up being the real fix?</em></p>
]]></content:encoded></item><item><title><![CDATA[How I Built a Native Safari MCP Server That Replaced Chrome in My AI Workflow]]></title><description><![CDATA[When I started building AI automation tools, I used Chrome DevTools MCP like everyone else. But running headless Chrome alongside Claude Code was eating 40-60% more CPU on my M1 Mac. My fans were scre]]></description><link>https://achiya-automation.hashnode.dev/how-i-built-a-native-safari-mcp-server-that-replaced-chrome-in-my-ai-workflow</link><guid isPermaLink="true">https://achiya-automation.hashnode.dev/how-i-built-a-native-safari-mcp-server-that-replaced-chrome-in-my-ai-workflow</guid><dc:creator><![CDATA[אחיה כהן]]></dc:creator><pubDate>Sun, 29 Mar 2026 19:41:34 GMT</pubDate><content:encoded><![CDATA[<p>When I started building AI automation tools, I used Chrome DevTools MCP like everyone else. But running headless Chrome alongside Claude Code was eating 40-60% more CPU on my M1 Mac. My fans were screaming.</p>
<p>So I built <strong>Safari MCP</strong> — a native macOS browser automation server that uses AppleScript instead of Chrome DevTools Protocol. Zero overhead, keeps logins, runs silently in the background.</p>
<h2>The Problem: Chrome Is Heavy</h2>
<p>Every Chrome-based automation tool (Puppeteer, Playwright, Chrome DevTools MCP) spawns a separate browser process. On Apple Silicon:</p>
<ul>
<li><strong>Chrome</strong>: 400-800MB RAM, constant CPU spikes</li>
<li><strong>Safari</strong>: Already running, 0 additional overhead</li>
</ul>
<h2>The Architecture: AppleScript + JavaScript Bridge</h2>
<p>Safari MCP uses a dual-engine approach:</p>
<p><strong>Engine 1: Safari Extension</strong> (~5-20ms per command) — HTTP polling bridge with full DOM access.</p>
<p><strong>Engine 2: AppleScript + Swift daemon</strong> (~5ms per command) — compiled Swift helper for screenshots, file dialogs, clipboard.</p>
<h2>80 Tools, Zero Configuration</h2>
<p>Navigation, interaction, reading, forms, storage, network monitoring — 80 tools that work transparently with both engines.</p>
<h2>The Hardest Bug: React Controlled Inputs</h2>
<p>React uses _valueTracker on inputs. The fix: set the tracker value, then set new value and dispatch input events. Works for React, Vue, Angular, and vanilla JS.</p>
<h2>Results</h2>
<ul>
<li><strong>CPU dropped 40-60%</strong> on Apple Silicon</li>
<li><strong>Fan noise eliminated</strong></li>
<li><strong>80 tools</strong> vs Chrome DevTools MCP 30</li>
<li>Running automation for <a href="https://achiya-automation.com">50+ business clients</a></li>
</ul>
<pre><code class="language-bash">npx safari-mcp
</code></pre>
<p><strong><a href="https://github.com/achiya-automation/safari-mcp">GitHub</a></strong> | <strong><a href="https://www.npmjs.com/package/safari-mcp">npm</a></strong></p>
<hr />
<p><em>I am Achiya Cohen, an automation engineer based in Israel. I build <a href="https://achiya-automation.com">WhatsApp bots, workflow automation, and AI tools</a> for businesses. <a href="https://achiya-automation.com/contact/">Let us talk</a>.</em></p>
<p><strong>What is the heaviest tool in your AI development stack? For me it was Chrome — what is yours?</strong></p>
]]></content:encoded></item><item><title><![CDATA[Every MCP Browser Tool Uses Chromium. Here's the Safari Alternative.]]></title><description><![CDATA[The MCP ecosystem has a browser monoculture problem.
I searched the MCP server registry. There are at least 13 browser automation servers listed. Every single one requires Chromium — Chrome DevTools P]]></description><link>https://achiya-automation.hashnode.dev/every-mcp-browser-tool-uses-chromium-here-s-the-safari-alternative</link><guid isPermaLink="true">https://achiya-automation.hashnode.dev/every-mcp-browser-tool-uses-chromium-here-s-the-safari-alternative</guid><dc:creator><![CDATA[אחיה כהן]]></dc:creator><pubDate>Sun, 29 Mar 2026 08:52:30 GMT</pubDate><content:encoded><![CDATA[<p>The MCP ecosystem has a browser monoculture problem.</p>
<p>I searched the MCP server registry. There are at least 13 browser automation servers listed. Every single one requires Chromium — Chrome DevTools Protocol, Puppeteer, Playwright with Chromium, or some wrapper around them.</p>
<p>On my MacBook, that meant a second browser process burning CPU while Safari sat right there with all my sessions already loaded.</p>
<p>## The Problem With Chrome-Based MCP Servers</p>
<p>Chrome with a debug port open idles at 8–15% CPU on an M2 MacBook Pro. During active automation it spikes to 25–40%. Chrome alone was consuming 200–400MB of RAM before a single page loaded.</p>
<p>Meanwhile, Safari was already open with all my logins — Gmail, GitHub, AWS console, client dashboards. I was running two browsers so my AI agent could interact with web pages.</p>
<p>## Safari MCP: The Native Alternative</p>
<p>I built [Safari MCP](<a href="https://github.com/achiya-automation/safari-mcp">https://github.com/achiya-automation/safari-mcp</a>) — 80 tools for native Safari browser automation via AppleScript + a Swift daemon.</p>
<p>**Key differences:**</p>
<p>| | Safari MCP | Chrome DevTools MCP | Playwright MCP |</p>
<p>|---|---|---|---|</p>
<p>| Your existing logins | Yes | Yes | No |</p>
<p>| CPU overhead | Minimal (native WebKit) | High (Chrome process) | Medium |</p>
<p>| Dependencies | None | Chrome + debug port | Playwright runtime |</p>
<p>| Tool count | 80 | ~30 | ~25 |</p>
<p>## What Changed When I Switched</p>
<p>The fans stopped. Safari MCP uses about 30MB of memory. Idle CPU at 0.1%. During active automation it bumps to 2–5%.</p>
<p>I stopped re-authenticating. The agent uses my real Safari with all existing sessions.</p>
<p>Safari stopped stealing my focus. Every command targets a specific tab by index.</p>
<p>## Getting Started</p>
<p>```bash</p>
<p>npm install safari-mcp</p>
<p>```</p>
<p>Add to your MCP config:</p>
<p>```json</p>
<p>{</p>
<p>"mcpServers": {</p>
<p>"safari": {</p>
<p>"command": "npx",</p>
<p>"args": ["safari-mcp"]</p>
<p>}</p>
<p>}</p>
<p>}</p>
<p>```</p>
<p>Works with Claude Code, Cursor, Windsurf, Claude Desktop, VS Code + Continue. MIT licensed.</p>
<p>**GitHub:** [github.com/achiya-automation/safari-mcp](<a href="https://github.com/achiya-automation/safari-mcp">https://github.com/achiya-automation/safari-mcp</a>)</p>
<p>---</p>
<p>*What's your CPU temperature when Chrome DevTools MCP runs alongside your IDE? I'm curious whether others have noticed the same overhead, or if it's a non-issue for most workflows.*</p>
]]></content:encoded></item><item><title><![CDATA[The Open-Source WhatsApp Automation Stack: WAHA + n8n + Chatwoot]]></title><description><![CDATA[If you're building WhatsApp automation for business clients, you've probably evaluated dozens of tools. After two years of deploying bots for small businesses in Israel — where 99% of the population u]]></description><link>https://achiya-automation.hashnode.dev/the-open-source-whatsapp-automation-stack-waha-n8n-chatwoot</link><guid isPermaLink="true">https://achiya-automation.hashnode.dev/the-open-source-whatsapp-automation-stack-waha-n8n-chatwoot</guid><category><![CDATA[WhatsApp automation]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[n8n]]></category><dc:creator><![CDATA[אחיה כהן]]></dc:creator><pubDate>Thu, 12 Mar 2026 17:50:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ad88e486766ac3a638d598/7e18cd1b-5346-44fb-b821-d9f02c08fd2e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're building WhatsApp automation for business clients, you've probably evaluated dozens of tools. After two years of deploying bots for small businesses in Israel — where 99% of the population uses WhatsApp daily — I've converged on an open-source stack that handles everything from message routing to human handoff.</p>
<p>Here's the architecture, why it works, and the trade-offs I've encountered.</p>
<h2>The Stack</h2>
<p><strong>WAHA (WhatsApp HTTP API)</strong> serves as the WhatsApp gateway. It's an open-source project that wraps WhatsApp Web into a clean REST API. You self-host it, so there are no per-message fees and you maintain full data ownership.</p>
<p><strong>n8n</strong> handles all workflow automation. Think of it as a self-hosted Zapier with a visual workflow builder, but with the flexibility to add custom code nodes when you need them. It connects WAHA's webhooks to your business logic — routing messages, querying databases, triggering notifications, and managing conversation state.</p>
<p><strong>Chatwoot</strong> provides the human support layer. When the bot can't handle a request, the conversation seamlessly transfers to a human agent with full context. Chatwoot gives you a proper support dashboard with conversation history, agent assignment, and SLA tracking.</p>
<p>The entire stack runs on a single VPS. No vendor lock-in, no per-message pricing surprises.</p>
<h2>Why This Combination Works</h2>
<h3>1. Full API Access Without Restrictions</h3>
<p>Unlike the official WhatsApp Business API (which requires BSP approval and has strict template requirements), WAHA gives you raw access to send and receive any message type — text, images, documents, locations, buttons, and lists. This matters when you need to build conversational flows that go beyond pre-approved templates.</p>
<p><strong>Important note:</strong> WAHA uses the unofficial WhatsApp Web protocol. It's not endorsed by Meta. For businesses that need official API compliance, the official Cloud API with a BSP is the right choice. But for many small business use cases, WAHA provides the flexibility needed at a fraction of the cost.</p>
<h3>2. Visual Workflow Building</h3>
<p>n8n's visual editor means you can prototype a bot flow in hours, not days. A typical appointment reminder workflow looks like this:</p>
<ol>
<li><p><strong>Trigger</strong>: Cron job checks for appointments 24 hours out</p>
</li>
<li><p><strong>Query</strong>: Fetch appointment details from database</p>
</li>
<li><p><strong>Send</strong>: WhatsApp message via WAHA with confirm/cancel buttons</p>
</li>
<li><p><strong>Listen</strong>: Webhook receives customer response</p>
</li>
<li><p><strong>Route</strong>: If confirmed → update status. If cancelled → offer slot to waitlist</p>
</li>
</ol>
<p>Each step is a visual node. Business logic changes don't require redeployment — just drag and reconnect.</p>
<h3>3. Seamless Bot-to-Human Handoff</h3>
<p>The most critical feature in any customer-facing bot is knowing when to stop being a bot. In our setup, typing "agent" at any point in the conversation triggers a handoff to Chatwoot. The human agent sees the full conversation history, the customer's profile, and any data the bot has already collected.</p>
<p>This isn't just a nice-to-have — <strong>78% of customers choose the first business that responds</strong> (Lead Connect). The bot ensures instant response at any hour, while Chatwoot ensures quality when human judgment is needed.</p>
<h2>Four Patterns That Drive Real ROI</h2>
<h3>Appointment Reminders</h3>
<p>Medical studies show automated reminders reduce no-show rates by <strong>40-60%</strong>. The key is making it conversational: let customers confirm, reschedule, or cancel with a single tap. If they cancel, automatically offer the slot to the next person on the waitlist.</p>
<h3>Lead Qualification at Scale</h3>
<p>Most small business leads arrive via WhatsApp outside business hours. A qualification bot asks targeted questions, identifies the service needed, provides pricing info, and schedules a callback. By morning, the sales team has warm, pre-qualified leads instead of cold messages.</p>
<h3>FAQ Automation</h3>
<p>Every business has 10-15 questions accounting for 80% of inquiries. A well-configured FAQ bot handles these instantly, 24/7, freeing human agents for complex interactions.</p>
<h3>Proactive Order Updates</h3>
<p>Automated status updates at each stage — confirmed, preparing, shipped, delivered — reduce "where is my order?" inquiries by up to 70%.</p>
<h2>Lessons from Production</h2>
<p><strong>Start with one use case.</strong> Businesses that try to automate everything at once end up with a bot that does nothing well. Begin with appointment reminders or FAQ, validate the ROI, then expand.</p>
<p><strong>Respect WhatsApp's personal nature.</strong> One promotional message too many and you get blocked — permanently. Every automated message should provide clear value.</p>
<p><strong>Data privacy matters.</strong> Self-hosted infrastructure means customer data stays on servers you control. For markets with strict data protection laws, this is a genuine compliance advantage.</p>
<p><strong>Monitor message delivery.</strong> WhatsApp occasionally changes its web protocol. WAHA updates usually follow quickly, but you need monitoring in place to catch delivery failures before your clients notice.</p>
<h2>The Bottom Line</h2>
<p>The gap between enterprise-grade automation and small business tools has closed. With WAHA, n8n, and Chatwoot, you can build sophisticated WhatsApp workflows on a single server, with no per-message fees and full data ownership.</p>
<p>For businesses where WhatsApp is the primary communication channel, this stack delivers instant response times, consistent customer experience, and measurable ROI — all while keeping costs predictable and data private.</p>
<hr />
<p><em>I'm Achiya Cohen, founder of</em> <a href="https://achiya-automation.com"><em>Achiya Automation</em></a><em>. I build custom WhatsApp bots and business automation solutions using open-source platforms, focusing on data privacy and tailored solutions for the Israeli market.</em></p>
]]></content:encoded></item></channel></rss>