ANTIGRAVITY LABJP
Articles/Editor View
Editor View/2026-09-17Advanced

How Far to Chase a VS Code Extension That Open VSX Doesn't Carry

Why your settings come across to Antigravity but your extensions don't, how to reconcile the gap with a script, and how to decide between a substitute, a pinned VSIX, moving the job outside the editor, or swapping the registry.

Open VSXExtensionsVSIXMigration7Antigravity371

Premium Article

I moved the maintenance environment for a client site over to Antigravity one evening. The first-run setup pulled my VS Code settings across, so I assumed the migration was done. What stopped me was saving a file the way I always do. No formatting ran. I opened the extensions view, and the things I was certain were installed simply weren't there.

The settings had come across. The extensions had not — two different things hiding under the same word, "import."

Here's what I checked that night, and how I decided what to do about the gap. My list held 34 extensions. Five of them, roughly 15%, had no counterpart on Open VSX. That's a small number, and it still took me two evenings to settle.

The settings came across; extensions were always on a different path

Antigravity is built from the VS Code codebase, so the layout and the keybindings feel familiar, and first-run setup can carry over your VS Code or Cursor configuration. That part matched what I expected.

Extensions, though, come from somewhere else entirely. Microsoft's Visual Studio Marketplace is offered for Microsoft's own products and isn't available to derived editors. Antigravity reads from Open VSX, the registry run by the Eclipse Foundation.

So what happens during migration isn't "the extensions disappeared." It's "the editor is looking at a different shelf." Different shelf, different books, different editions. Miss that, and you spend the evening reinstalling the same thing repeatedly. I did exactly that for the first half hour.

Some IDs sit on both shelves. Some sit on only one. And the awkward third case: the ID matches, but the thing behind it came from somewhere else. That third case is what I overlooked first.

Hand the comparison to a script, not your eyes

Scrolling the extensions view and noting what's absent falls apart somewhere past twenty entries. I dumped the ID list from the old environment instead, and queried the Open VSX public API for each one.

Open VSX returns extension metadata as JSON at https://open-vsx.org/api/{publisher}/{name}, and a 404 when nothing is there. That's enough to decide.

#!/usr/bin/env python3
"""reconcile_extensions.py — reconcile a local extension list against Open VSX.
 
  prepare: run  code --list-extensions > installed.txt  in the old environment
  run:     python3 reconcile_extensions.py installed.txt > ledger.tsv
 
Output is TSV. Rows where state is "absent" are the ones you have to decide about.
"""
import json
import sys
import time
import urllib.error
import urllib.request
 
API = "https://open-vsx.org/api/{ns}/{name}"
UA = {"User-Agent": "ext-reconcile/1.0 (personal migration audit)"}
 
 
def lookup(ext_id):
    ns, _, name = ext_id.partition(".")
    if not name:
        # drop blank lines and comments that aren't in publisher.name form
        return {"state": "invalid", "note": "not in publisher.name form"}
 
    req = urllib.request.Request(API.format(ns=ns, name=name), headers=UA)
    try:
        with urllib.request.urlopen(req, timeout=10) as res:
            data = json.load(res)
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return {"state": "absent", "note": "no matching ID on Open VSX"}
        if e.code == 429:
            # throttled: wait once, then retry a single time
            time.sleep(5)
            return lookup(ext_id)
        return {"state": "error", "note": "HTTP {}".format(e.code)}
    except urllib.error.URLError as e:
        return {"state": "error", "note": str(e.reason)}
 
    published_by = (data.get("publishedBy") or {}).get("loginName", "")
    access = data.get("namespaceAccess", "")
    # an unclaimed namespace means someone other than the owner could have published here
    state = "present" if access == "restricted" else "present-unclaimed"
    return {
        "state": state,
        "version": data.get("version", ""),
        "published_by": published_by,
        "access": access,
        "note": "",
    }
 
 
def main(path):
    print("\t".join(["ext_id", "state", "version", "published_by", "access", "note"]))
    for line in open(path, encoding="utf-8"):
        ext_id = line.strip()
        if not ext_id or ext_id.startswith("#"):
            continue
        r = lookup(ext_id)
        print("\t".join([
            ext_id,
            r.get("state", ""),
            r.get("version", ""),
            r.get("published_by", ""),
            r.get("access", ""),
            r.get("note", ""),
        ]))
        time.sleep(0.4)  # keep the public API request rate polite
 
 
if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "installed.txt")

Expected output looks like this:

ext_id	state	version	published_by	access	note
esbenp.prettier-vscode	present	11.0.0	open-vsx-bot	restricted
dbaeumer.vscode-eslint	present	3.0.10	microsoft	restricted
ms-azuretools.vscode-docker	absent				no matching ID on Open VSX

The time.sleep call is there so you don't throw dozens of requests at a public API in a few seconds. Thirty extensions finish in well under a minute, and the TSV becomes the basis for everything that follows.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You'll be able to reconcile your extension list against Open VSX automatically and sort what's missing into 3 buckets, with the publisher verified
You'll be able to pick between a substitute extension, a pinned VSIX, moving the job outside the editor, or swapping the registry, based on what your repo actually needs
You'll know what to check before installing a third-party republish that merely shares the same ID, so you don't reinstall across every machine later
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Editor View2026-03-16
Antigravity Lab — Cursor to Antigravity Migration Guide 2026
2026 migration guide: Switch from Cursor to Antigravity. Complete comparison, migration steps, and solutions to common challenges. Why Antigravity is the Cursor alternative.
Editor View2026-09-10
The File Counts Matched. 148 Pairs Disagreed on Code Blocks
My Japanese and English article trees matched at 1,057 files each, yet the bodies had quietly diverged. Here is what counting code blocks and H2 headings across every pair turned up, why a character-length ratio failed to catch any of it, and the 40-line checker I now run before every push.
Editor View2026-09-03
How much of Antigravity's generated commit message I actually keep
Where the Review pane's generated commit message stops being enough, and where I start writing. A prepare-commit-msg hook that adds a Why trailer, plus measured behaviour during rebase, cherry-pick and revert.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links