{"slug": "i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root", "title": "I filed a critical bug against my own tool. Then I read the code — and my own root cause was wrong.", "summary": "A developer filed a critical bug against their own tool, safari-mcp, after discovering that an AI agent could navigate user tabs. However, upon reading the code, they found that the proposed fix had already been implemented three months prior, and the real bug was a different issue in the same function. The bug was a 'ghost proactive fix' that clamped an out-of-range tab index to the current tab count, potentially causing the agent to target the wrong tab.", "body_md": "Three days ago I filed the most serious issue my project has ever had — against myself.\n\nThe tool is [safari-mcp](https://github.com/achiya-automation/safari-mcp), an MCP server that lets an AI agent drive the Safari you're already logged into. That premise is the whole value proposition, and it's also the whole danger: **the user is using this browser at the same time as the agent.** The single promise the project makes is \"the agent never touches a tab it didn't open.\"\n\nThe issue was that the promise had broken. Two of a user's tabs ended up displaying pages the agent had loaded. Nothing was closed — the back-history survived — but scroll position, in-page state, anything unsaved: gone.\n\nI wrote up the incident, traced the root cause, and proposed a fix. Then today I opened the file to actually write that fix.\n\n**My root cause was wrong.** The fix I proposed had already shipped, three months ago. And the real bug was sitting four lines below it, wearing the word \"fix\" in its own log message.\n\nHere's what I wrote in the issue:\n\n`_ownedTabs`\n\n(and every`tabIndex`\n\nparameter) is a positional handle to a mutable, user-controlled list. Positional handles are only valid as long as nobody else mutates the list.\n\nAnd the proposed fix:\n\nTrack ownership by a\n\nstable identity, not a position. On`safari_new_tab`\n\n, inject a sentinel into the page (e.g.`window.__safariMcpTabId = \"<uuid>\"`\n\n), and resolve`tabIndex`\n\n→ real tab by scanning windows for the matching sentinel.\n\nConfident. Specific. Reasonable. I even left a follow-up comment calling the sentinel design \"the plan of record.\"\n\nI wrote all of that from my memory of the architecture. I did not open `safari.js`\n\n.\n\nThe sentinel already existed. It had been there since v2.8.3, released April 14 — a release literally titled *\"bulletproof tab tracking via window.__mcpTabMarker.\"*\n\n```\n// ========== TAB IDENTITY MARKER ==========\n//  - window.name           : survives EVERY navigation (full loads, redirects,\n//                            cross-origin). The browser preserves window.name by\n//                            design — the bulletproof identity that index/URL lack.\n//  - window.__mcpTabMarker : survives SPA / same-document routing (secondary marker).\n```\n\nI had written the fix I was now proposing. I'd written the *comment explaining why it was the right fix*. I'd forgotten I'd done it.\n\nSo the interesting question stopped being \"why is ownership positional\" — it isn't — and became **\"if identity resolution is already there, how did a user's tab still get navigated?\"**\n\n`resolveActiveTab()`\n\nis the function that answers \"which tab is ours, right now?\" It has a strategy ladder, and what matters is how each rung *fails*.\n\n**Rung 1 — the marker scan.** Loop every tab, ask each one whether `window.name`\n\nmatches our marker. Found it? That's our tab, whatever index it's sitting at. This is identity, and it's correct.\n\n**Rung 2 — no URL to fall back on:**\n\n```\nif (_st().hasOwnedTab && !_st().activeTabMarker) { _st().activeTabIndex = null; }\n```\n\nIdentity lost → drop the index. **Fails closed.** ✅\n\n**Rung 3 — the URL scan comes back empty:**\n\n```\nif (_st().hasOwnedTab && !_st().activeTabMarker) {\n  console.error('[Safari MCP] Tab identity lost (marker + URL unresolved) — clearing index to avoid targeting the user\\'s tab');\n  _st().activeTabIndex = null;\n  return null;\n}\n```\n\nSame call, same instinct. **Fails closed.** ✅\n\nAnd then, four lines later, in the same block:\n\n```\nif (_st().activeTabIndex && _st().activeTabIndex > tabCount) {\n  console.error(`[Safari MCP] Tab ghost proactive fix: index ${_st().activeTabIndex} > tabCount ${tabCount}, clamping to ${tabCount}`);\n  _st().activeTabIndex = tabCount;\n}\n```\n\nRead that carefully, because I didn't for three months.\n\nWe tracked tab 8. The window now has 7 tabs — because the user closed one, or tore one into its own window. Our index is out of range. That is the *exact* moment the code has learned it no longer knows where our tab is.\n\nAnd it responds by **clamping the index to tabCount**: tab 7. The last tab in the window. A tab we have never seen, that belongs to the user, chosen for no reason other than that it's the highest index that won't throw.\n\nThen it logs the words **\"proactive fix.\"**\n\nThat's the bug. Not positional ownership — a fail-open sitting between two fail-closed branches, in the safety-critical path, describing itself as a fix.\n\n`git log -S`\n\nputs the clamp at March 31. The identity marker landed April 14.\n\nThe clamp is **two weeks older than the mechanism that made it obsolete.** It's from the era when the index was genuinely all we had, when \"out of range\" produced ugly AppleScript errors and clamping made them stop.\n\nAnd that's the actual lesson, the one that generalizes past my weird little macOS project:\n\n**When you add a better mechanism, the old heuristic does not remove itself.**\n\nThe v2.8.3 work added identity resolution and correctly rewired the branches it was looking at — the two `hasOwnedTab && !activeTabMarker`\n\nguards *are* the new thinking, and they fail closed because in April I understood the stakes. The clamp wasn't rewired, because it didn't look like an ownership decision. It looked like input validation. It looked like the *careful* line. It had a bounds check and an error log.\n\nEvery upgrade leaves fossils like this. The dangerous ones aren't the code that looks scary. They're the code that looks like it's on your side.\n\nHere's the tell I want to hand you, because it's cheap and it's reusable.\n\nThe clamp was written to fix a *symptom*: an index pointing past the end of the array. It makes that symptom disappear by **inventing a plausible value**. `tabCount`\n\nisn't a computed answer to \"where is our tab\" — it's the nearest number that doesn't crash.\n\nAny code that converts \"I don't know\" into a plausible value is a fail-open wearing a fix's clothes.\n\nOnce you have that phrasing, you start seeing them everywhere. `?? 0`\n\non a total that should have been fetched. `catch {}`\n\naround the call that establishes permission. `|| user[0]`\n\nwhen the lookup missed. Clamping an index into range. Each one takes a state where the honest answer is *stop* and launders it into a value the next line will happily use.\n\nThe bounds check is real. The clamp is the bug. They're on the same line, and that's exactly why it survived three months of me reading past it.\n\nIt's a deletion, not the sentinel architecture I proposed. That branch has to do what its two neighbours already do — when the index is out of range, identity is lost, so drop it and make the caller re-anchor:\n\n```\nif (_st().activeTabIndex && _st().activeTabIndex > tabCount) {\n  _st().activeTabIndex = null;   // fail closed, like every other exit\n  return null;\n}\n```\n\nCallers already handle this. There's a `_assertNotFallingBackToUserTab()`\n\nthat throws a clear *\"Tab tracking lost — re-run safari_new_tab to recover\"*. The recovery path was built. The clamp was just routing around it.\n\nThe cost is honest: sessions that used to get lucky will now throw. In a tool whose entire promise is *\"we don't touch your tabs,\"* an error message is the correct output for \"I don't know which tab is mine.\" A guess is not.\n\nIt's [PR #59](https://github.com/achiya-automation/safari-mcp/pull/59), open against [issue #54](https://github.com/achiya-automation/safari-mcp/issues/54) — where I've also left my original root-cause analysis standing, wrong, with a correction under it. The drift between what I remembered and what shipped *is* the bug; editing the evidence out seemed like the wrong move.\n\nI filed a detailed, confident, well-structured bug report about code I wrote, and got the root cause wrong — because I reasoned from my mental model instead of from the file. The mental model was a year of accumulated intent. The file was what actually shipped. Those had quietly drifted apart, and the gap between them is precisely where the bug lived.\n\nIf I'd handed that issue to an AI agent — or a new contributor — they'd have implemented the sentinel I asked for. Diligently. It already existed. The clamp would still be there, and the user's tabs would still be getting navigated, and the issue would be closed.\n\n**The most expensive thing in that whole chain was my confidence.**\n\nHave you found one of these in your own code — a \"fix\" that was actually inventing an answer? I'd genuinely like to collect the pattern. Drop it in the comments.\n\n[safari-mcp](https://github.com/achiya-automation/safari-mcp) is MIT-licensed and drives your real, logged-in Safari on macOS. It has 97 tools, and — once #59 lands — one fewer place where it guesses which tab is yours.", "url": "https://wpnews.pro/news/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root", "canonical_source": "https://dev.to/achiya-automation/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root-cause-was-wrong-3692", "published_at": "2026-07-17 07:19:32+00:00", "updated_at": "2026-07-17 07:31:18.321470+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-safety"], "entities": ["safari-mcp", "achiya-automation"], "alternates": {"html": "https://wpnews.pro/news/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root", "markdown": "https://wpnews.pro/news/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root.md", "text": "https://wpnews.pro/news/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root.txt", "jsonld": "https://wpnews.pro/news/i-filed-a-critical-bug-against-my-own-tool-then-i-read-the-code-and-my-own-root.jsonld"}}