<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gilles W</title>
    <description>The latest articles on DEV Community by Gilles W (@gilles_w_e3d81d46024d1269).</description>
    <link>https://dev.to/gilles_w_e3d81d46024d1269</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995347%2Fcaf91794-72ea-4cb6-bbfb-b28311ffea31.png</url>
      <title>DEV Community: Gilles W</title>
      <link>https://dev.to/gilles_w_e3d81d46024d1269</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gilles_w_e3d81d46024d1269"/>
    <language>en</language>
    <item>
      <title>I built a Chrome tab manager that cannot make a network request</title>
      <dc:creator>Gilles W</dc:creator>
      <pubDate>Sun, 21 Jun 2026 13:33:35 +0000</pubDate>
      <link>https://dev.to/gilles_w_e3d81d46024d1269/i-built-a-chrome-tab-manager-that-cannot-make-a-network-request-g65</link>
      <guid>https://dev.to/gilles_w_e3d81d46024d1269/i-built-a-chrome-tab-manager-that-cannot-make-a-network-request-g65</guid>
      <description>&lt;p&gt;In 2021 the Great Suspender — a beloved tab-suspending extension with millions of users — was quietly sold to a new owner, who shipped an update that turned it into adware/malware. Google eventually pulled it and force-disabled it for everyone. The unsettling part wasn't that one extension went bad. It was the realization that "it's just a tab manager" was never actually safe: the thing had the permissions to read every page you visited, and one ownership change was all it took to use them.&lt;/p&gt;

&lt;p&gt;So when I wanted a tab manager, I didn't want a &lt;em&gt;promise&lt;/em&gt; that it wouldn't phone home. I wanted a tool that &lt;strong&gt;structurally cannot&lt;/strong&gt;. The privacy should be a property of the code, not a line in a privacy policy.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Stowtab&lt;/strong&gt;, and the whole pitch is that you don't have to trust me — you can read it in an afternoon. It's MIT, exactly &lt;strong&gt;3 permissions&lt;/strong&gt;, &lt;strong&gt;zero host permissions&lt;/strong&gt;, and &lt;strong&gt;zero network calls&lt;/strong&gt;. Here are the two engineering decisions that make those claims verifiable rather than aspirational.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Status note up front, because honesty is the entire point of this project: Stowtab is &lt;strong&gt;not on the Chrome Web Store yet&lt;/strong&gt; — that one-click listing is in review. Today you install it by loading it unpacked from the &lt;a href="https://github.com/gilleswainrib-ext-boop/stowtab" rel="noopener noreferrer"&gt;public repo&lt;/a&gt; (30-second steps in the README). The one-click version is coming soon; there's a &lt;a href="https://stowtab.surge.sh/?ref=devto" rel="noopener noreferrer"&gt;waitlist&lt;/a&gt; if you'd rather wait for the button.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Delete the attack surface: no host permissions
&lt;/h2&gt;

&lt;p&gt;The entire trust pitch is the manifest. Here's the relevant part, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tabs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tabGroups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content_security_policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extension_pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"script-src 'self'; object-src 'self'"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;host_permissions&lt;/code&gt;. No &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;. No &lt;code&gt;scripting&lt;/code&gt;. No content scripts. That's not minimalism for its own sake — each of those omissions removes a capability that the extension then provably doesn't have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It can enumerate tabs (title, URL, group) through the &lt;code&gt;tabs&lt;/code&gt; API, but it &lt;strong&gt;cannot read page content or touch the DOM&lt;/strong&gt;. Reading pages requires &lt;code&gt;scripting&lt;/code&gt; + host permissions, which Stowtab never requests. If a future "update" wanted to start scraping your pages, it couldn't do it silently — adding those permissions triggers a new, scary install-time consent prompt.&lt;/li&gt;
&lt;li&gt;With &lt;strong&gt;no host permissions, there is no origin the extension is allowed to fetch.&lt;/strong&gt; That's the load-bearing detail. "No network calls" isn't a policy I'm asking you to believe — it's the &lt;em&gt;absence of a capability&lt;/em&gt;. There's no host it's permitted to reach, and the CSP (&lt;code&gt;script-src 'self'&lt;/code&gt;) means no remote code can be injected to work around that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Storage is local-only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// everything (sessions, settings, license) lives here — never storage.sync&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deliberately avoided &lt;code&gt;chrome.storage.sync&lt;/code&gt; so nothing silently rides up to Google's servers. And suspend uses the native &lt;code&gt;chrome.tabs.discard()&lt;/code&gt; rather than the old Great-Suspender trick of redirecting each tab to an extension-hosted page — so there's no interception layer sitting between you and your tabs, and discarded tabs restore natively.&lt;/p&gt;

&lt;p&gt;The result: the most private design also turned out to be the &lt;em&gt;simplest&lt;/em&gt; one. You don't have to be trusted not to abuse a capability you never asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Monetize without a server: offline Ed25519 licenses
&lt;/h2&gt;

&lt;p&gt;Here's the fun constraint. A tool whose entire identity is "never phones home" obviously can't have a license-activation server to call. So how do you sell a $19 Pro tier without a backend?&lt;/p&gt;

&lt;p&gt;You sign the licenses offline and verify them locally with WebCrypto.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mint (server-side, one private key that never ships):&lt;/strong&gt; a payload like &lt;code&gt;{ tier: "pro", emailHash, issuedAt }&lt;/code&gt; gets signed with an &lt;strong&gt;Ed25519&lt;/strong&gt; private key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify (in the extension):&lt;/strong&gt; the package ships only the &lt;strong&gt;public&lt;/strong&gt; key. The extension checks the signature with &lt;code&gt;crypto.subtle.verify&lt;/code&gt; and the native &lt;code&gt;Ed25519&lt;/code&gt; algorithm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The verification path, lightly trimmed from &lt;code&gt;src/lib/license.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LICENSE_PUBLIC_KEY_RAW_B64&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./constants.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;_pub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pubKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;_pub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;_pub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;b64ToBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LICENSE_PUBLIC_KEY_RAW_B64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ed25519&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;_pub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STOW1.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;b64urlToBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payloadBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;b64ToBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ed25519&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pubKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;b64ToBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;payloadBytes&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payloadBytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// fail closed&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details I like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It re-verifies on every check&lt;/strong&gt; (&lt;code&gt;isPro()&lt;/code&gt; re-runs &lt;code&gt;verifyKey&lt;/code&gt; against the stored key), so hand-editing &lt;code&gt;chrome.storage.local&lt;/code&gt; to flip yourself to Pro just fails the signature check and reverts to Free. It fails &lt;em&gt;closed&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The public key is &lt;strong&gt;safe to ship&lt;/strong&gt; — that's the point of asymmetric signing. You can read it in the repo and confirm there's no secret hiding in the bundle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the honest trade-off, which is right there in a code comment so I can't pretend otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stops casual fakes &amp;amp; storage tampering; does not stop a determined user from sharing a key.&lt;/span&gt;
&lt;span class="c1"&gt;// No network calls ever — consistent with the "never phones home" promise.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Offline keys can't be revoked and can't stop key-sharing. For a $19 trust-first tool, that's the correct call — aggressive DRM would require exactly the kind of phone-home machinery the whole product exists to avoid. I'd rather lose a few bucks to sharing than contradict the premise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does (so it's not just a privacy stunt)
&lt;/h2&gt;

&lt;p&gt;A privacy model nobody uses is worthless, so the free tier is genuinely useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Group tabs by domain&lt;/strong&gt; in one click&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Find and close duplicate tabs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suspend tabs&lt;/strong&gt; via native &lt;code&gt;chrome.tabs.discard()&lt;/code&gt; to reclaim memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy search&lt;/strong&gt; across every open tab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save / restore sessions&lt;/strong&gt; (Free: 3; Pro: unlimited)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted local backup&lt;/strong&gt; (Pro)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pro is a &lt;strong&gt;$19 one-time&lt;/strong&gt; unlock — no subscription, no account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify, don't trust
&lt;/h2&gt;

&lt;p&gt;That's the whole ethos. Don't take my word for any of the above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the manifest and confirm there are no host permissions.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;src/lib/license.js&lt;/code&gt; and confirm the verification is offline.&lt;/li&gt;
&lt;li&gt;Open DevTools → Network while you use it and watch nothing happen.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source (MIT) + 30-second load-unpacked steps:&lt;/strong&gt; &lt;a href="https://github.com/gilleswainrib-ext-boop/stowtab" rel="noopener noreferrer"&gt;https://github.com/gilleswainrib-ext-boop/stowtab&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landing + waitlist for the one-click Web Store version:&lt;/strong&gt; &lt;a href="https://stowtab.surge.sh/?ref=devto" rel="noopener noreferrer"&gt;https://stowtab.surge.sh/?ref=devto&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find a hole in the permission model, I genuinely want to hear it — that's the most useful comment I could get. The brand is honesty, so poke hard.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I built Stowtab. It's a commercial product with a free open-source core. Links above are my own.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>chromeprivacyjavascripchrome</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
