Author & sole developer
A Manifest V3 extension that takes Instagram apart from the inside — the feed, the receipts, the metrics — one switch at a time.

IGFocus is a Manifest V3 extension that rewrites Instagram's behaviour from inside the tab it is already running in — thirty-three independent features, four presets, twelve locales with right-to-left, and no account, no server, no analytics and no third-party script anywhere in the bundle. The feed, Reels, Stories, Explore, comments and every vanity metric come out on a switch. DMs are read without sending “Seen” and stories are watched without joining the viewer list. Full-resolution media is recovered and saved, a Reel's soundtrack is transcoded to MP3 in the tab, and an entire conversation is written to one self-contained searchable HTML file. Every feature in the product is declared as data in a single config module, and the popup, the presets, the storage schema and the content-script gating are all generated from that declaration rather than restated four times.
The architecture problem underneath it is that an MV3 extension has no continuous process. Service workers and content scripts are both killed when idle, so nothing may be held in memory across events: state is `chrome.storage.sync`, re-read on wake, and both ends `watch` the same keys so the popup and the page converge without a message protocol between them. Every visual feature is expressed as a class on `<html>` plus two CSS custom properties gated behind one root class, which makes disabling the extension a single class removal rather than a teardown pass — and makes the cost of a feature a stylesheet rule instead of a DOM mutation. What no selector can express — “the story tray”, “a suggested post”, “a comment thread” — is resolved in JS and tagged with a marker class, and those marks are recomputed from scratch on every pass rather than assigned once and trusted, because Instagram recycles DOM nodes across in-app navigations: a `<ul>` that was a comment thread on the feed is a photo grid on a profile one route later. Recomputation is what lets a mark leave a node the moment it stops being what it was, and it is why the pass reads geometry before it clears anything — otherwise a hidden element flashes back for a frame on every mutation.
Ghost Mode is the part that required real network work, because it is the only feature that changes what Instagram is *told* rather than what you see. Presence is reported over four independent transports, so there are four blockers compiled from one declaration. Plain REST receipts are dropped by `declarativeNetRequest` rules that live in the browser's own rule store rather than in the worker, so they outlive the worker being killed, with rule ids derived from each signal's position so a toggle always owns its ids and a stale rule cannot survive it. `/api/graphql` cannot be filtered by URL at all — a “mark thread read” mutation is batched down the same path as the query that fetched the messages, so blocking the URL breaks the inbox and only the payload can separate them — which forces a `world: "MAIN"` interceptor patching `fetch`, `XMLHttpRequest`, `sendBeacon` and `WebSocket.prototype.send`, in every frame, because a same-origin iframe is a route to an unpatched `fetch`. The typing beacon rides Instagram's MQTT-over-WebSocket connection, so individual binary frames are dropped by scanning the buffer for the topic, which an MQTT packet carries as plain UTF-8 ahead of a payload that may well be compressed — the connection itself stays open. The fourth is the one that made the first three insufficient: a DM read receipt is sent twice, and the second copy comes from the SharedWorker backing the inbox, which has its own global scope and its own `fetch` and never saw any of the patches. The `Worker` and `SharedWorker` constructors are wrapped to boot from a blob that installs the same interceptor and then imports Instagram's real bundle; a blob URL inherits the page's origin, so cookies and network access are unchanged, and the shim is only armed while a signal that needs it is on, because a blob-keyed `SharedWorker` is no longer shared between tabs.
The two halves cannot share storage — the page world has no `chrome.*` — so the armed-signal set crosses on `window.postMessage` and later changes are pushed into already-running workers over a `BroadcastChannel`. The scripts start in no guaranteed order, so the page-world half announces itself and the isolated half answers, and it stays silent until storage has actually resolved: replying with the empty defaults would disarm the patches at precisely the moment they are meant to arm. A request that beats the settings across is held for up to 1.5s rather than let through, and that grace expires exactly once. The whole system is written to fail open, because the two failure modes are not symmetric: a receipt that slips through is a privacy miss, while a pattern greedy enough to swallow the request that *sends* a message is a silently broken inbox — so anything shaped like a send is never dropped, whatever is batched alongside it. The first release proved the other edge of that: it matched DM receipts on a `Direct*` prefix Instagram's web client does not use, nothing matched, and because the design fails open nothing looked wrong. The patterns are shape-based now, and not one of them uses `\b` — these operation names arrive glued into camel case, where every word boundary sits between two word characters and the anchor can never fire.
The downloader never touches the pixels on screen, because they are not the asset: what renders is a re-encode chosen for your viewport, and a Reel's `<video>` holds a `blob:` URL bound to a MediaSource that dies with it. The originals only ever exist in the JSON Instagram fetched to build the page, so a page-world sniffer keeps a catalog of them under a strict read-only contract — `fetch` responses are `clone()`d so the page's own body is never consumed, XHR is read off its `load` event, nothing is blocked, rewritten, delayed or re-requested, and every path is wrapped, because a sniffer that throws is worse than one that misses. Harvesting is a deep structural walk rather than a set of known paths: Instagram serves the same media through REST, three generations of GraphQL and whatever is being migrated to this quarter, and while the envelopes churn constantly the leaves have outlived all of them. Resolution back from a DOM element goes by URL first — the CDN re-signs its query string per request but keeps the filename, and the size lives in the path, so the 640px `<img src>` on screen matches the 1080px original in the catalog — then by shortcode, then by whatever the DOM can prove. MP3 extraction is the one path that genuinely needs the bytes, so cross-origin access to the media host is an optional permission requested at the moment the switch is thrown; denied leaves the switch off rather than on and silently failing.
The chat archive is a single pure function — thread in, whole document out — and it is inert twice over, because every character in it came out of someone's messages. The transcript is embedded once as JSON in a `<script type="application/json">` block with `<`, `>`, `&` and both Unicode line separators escaped, so a message containing `</script>` cannot close its own tag, and the page's own script builds every node with `createElement` and `textContent`, never `innerHTML` — including the search highlighter, the one place text is taken apart and reassembled. Self-contained is asserted rather than intended: no stylesheet link, no font host, no CDN, no images. Fourteen headless checks stand behind the build and none of them mock the thing under test. One compiles the Ghost interceptor exactly as production does, minifier included, drops it into a stand-in page, pushes a receipt down every transport, then takes the blob it hands the SharedWorker and executes *that* in a stand-in worker. Another renders an archive from a thread whose title, messages, reactions and replies are all attacks, opens it with scripting enabled, and asserts `window.pwned` is still undefined — including after searching for the markup, which is when the highlighter has to handle it.