Cursor vs VS Code Extensions: 13-Step Audit, 40 Min [2026]

Most Cursor vs VS Code extensions problems start with one wrong assumption: that both editors read from the same shelf. They do not. VS Code installs from Microsoft’s marketplace, and Cursor uses the Open VSX registry, which its own help page says carries many popular extensions but not necessarily all of them, and not always in identical form. If your team shares one extension list across both editors, some entries will quietly fail, and nobody will notice until a linter, a language server or a remote workflow stops working.

This tutorial builds a small audit tool that finds those gaps before they bite. It reads your VS Code extension list, your devcontainer.json and your .vscode/extensions.json, asks both registries about every ID, and prints a verdict per extension. The whole project is one Python file with no third-party dependencies, six unit tests, a CI workflow and a Makefile. Plan on 13 steps and about 40 minutes, and the only cost is your time.

I ran the finished tool against a 12-extension sample on October 24, 2026. Four IDs were missing from Open VSX, one trailed the Marketplace build by 224 days, and six matched. If you already moved a project over using the VS Code extensions in Cursor setup guide, this audit is the check that guide cannot do for you, because registries change daily and your list keeps growing.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Cursor vs VS Code Extensions Drift Apart

Both editors share the same extension API, but the shelf each one pulls from differs. The Eclipse Foundation describes Open VSX as “the open source, vendor-neutral extension registry for tools built on the VS Code extension API.” Publishers must push their builds there separately from the Microsoft marketplace, and plenty of them push late, push only some releases, or never push at all. Microsoft’s own remote and language extensions are the best-known examples, since those are built for Microsoft’s editor builds.

Open VSX itself is healthy. Eclipse lists Open VSX 1.3.0 as a minor server release dated October 16, 2026, and the public registry stays free for publishing, search and downloads. The gap is not a registry outage. It is a publishing gap that sits on the extension author’s side, and you only see it by comparing the two shelves side by side. If you maintain extensions yourself, the guide to publishing VS Code extensions to Open VSX shows the other half of this problem.

Dev containers add a second layer. Microsoft’s documentation says a “devcontainer.json file in your project tells VS Code how to access (or create) a development container with a well-defined tool and runtime stack.” That file also carries an extension list, so every teammate or CI job that opens the container in Cursor inherits any gap in it. A third-party reference guide from ForestsNet reports that Cursor has shipped its own Remote SSH, Dev Containers and WSL extensions since May 2025 instead of relying on Microsoft’s, which would explain why Microsoft’s remote extensions show up as absent in the audit while remote work in Cursor can still function.

The audit answers three questions for every extension ID:

  • Does the ID exist on Open VSX at all?
  • If it does, how far behind the Marketplace build is it, measured in days rather than version strings?
  • Does the Open VSX build demand a newer VS Code engine than your editor ships?

A fourth question, whether you trust the publisher, stays with you. The tool gives you data. It does not give you a security review.

Prerequisites and Versions

You need very little. The tool uses only the Python standard library, so there is no virtual environment to fight with. Network access is the one real requirement, because the script calls two public HTTPS endpoints. If you already followed the VS Code to Cursor migration walkthrough, you have everything below.

ComponentVersionWhy you need it
Python3.10 or newer (tested on 3.12.3)Runs the audit script and unit tests
VS CodeLatest stable, code command on PATHExports the list of installed extensions
CursorLatest stable, cursor command on PATHInstalls the extensions that pass the audit
GitAny recent releaseTracks the allowlist and fixtures
DockerOptional, only for dev container testingLets you open devcontainer.json in a real container
NetworkHTTPS to marketplace.visualstudio.com and open-vsx.orgBoth registries are queried live

Two shell commands matter here. In VS Code, open the command palette and run Shell Command: Install ‘code’ command in PATH. Cursor offers the equivalent for its own cursor command. Check both before you start:

python3 --version
code --version
cursor --version

If either editor command prints “command not found,” fix that first. Everything downstream depends on both CLIs. Budget about 40 minutes for the full 13 steps, and add 10 more if you want to test a real dev container.

Steps 1 and 2: Create the Project and Export Your VS Code Extension List

Step 1 sets up a folder with the layout the rest of the tutorial fills in. Keep fixtures and tests separate from the script, because the fixtures double as the inputs for your first runs.

mkdir extparity && cd extparity
git init
mkdir -p fixtures tests .github/workflows
touch extparity.py

Open extparity.py and paste the header below. It defines the two registry URLs, a user agent string, a six-hour cache lifetime, and a regular expression that decides what counts as a valid extension ID. The http_json helper handles retries for rate limits and server errors, and it respects a Retry-After header when the server sends one. That matters, because a registry that slows you down once is easy to forgive and one that bans you is not.

#!/usr/bin/env python3
"""extparity: compare VS Code Marketplace and Open VSX for a list of extensions."""
import argparse
import concurrent.futures as cf
import csv
import json
import os
import re
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone

MARKETPLACE = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery"
OPENVSX = "https://open-vsx.org/api"
UA = "extparity/1.0 (extension parity audit)"
CACHE_PATH = os.path.expanduser("~/.cache/extparity.json")
CACHE_TTL = 6 * 3600
ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9._-]*$", re.I)


def http_json(url, body=None, headers=None, retries=3):
    data = json.dumps(body).encode() if body is not None else None
    hdr = {"User-Agent": UA, "Accept": "application/json"}
    if body is not None:
        hdr["Content-Type"] = "application/json"
    hdr.update(headers or {})
    for attempt in range(retries):
        req = urllib.request.Request(url, data=data, headers=hdr)
        try:
            with urllib.request.urlopen(req, timeout=20) as resp:
                return resp.status, json.load(resp)
        except urllib.error.HTTPError as err:
            if err.code == 429 or err.code >= 500:
                wait = int(err.headers.get("Retry-After", 2 ** attempt))
                time.sleep(min(wait, 30))
                continue
            try:
                return err.code, json.load(err)
            except ValueError:
                return err.code, {}
        except (urllib.error.URLError, TimeoutError):
            time.sleep(2 ** attempt)
    return 0, {}


def parse_ts(stamp):
    return datetime.strptime(stamp[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)

Step 2 exports the extension list. VS Code prints one ID per line, and --show-versions appends the installed version after an at sign:

code --list-extensions --show-versions > fixtures/vscode-extensions.txt

For the examples in this article I used a deliberately mixed sample of ten extensions, including several Microsoft-published ones, because that is where gaps usually hide:

# output of: code --list-extensions --show-versions
dbaeumer.vscode-eslint
esbenp.prettier-vscode
eamodio.gitlens
ms-python.python
ms-python.vscode-pylance
ms-vscode.cpptools
ms-vscode-remote.remote-ssh
ms-azuretools.vscode-docker
redhat.java
golang.go

One detail catches people out. If you run that export on your host machine, it lists only host-side extensions. Extensions installed inside a dev container live in the container’s own server directory, so run the same command from a terminal inside the container if you want the real picture of what that environment loaded.

Steps 3 and 4: Build the Marketplace and Open VSX Clients

Step 3 adds the Marketplace lookup. The editors talk to a public gallery endpoint, and a POST with a filter on the extension name returns the newest version, its last-updated timestamp and a property list. A set of request flags asks for the latest version only, which keeps the response small and is kind to both sides.



def marketplace_lookup(ext_id):
    body = {
        "filters": [{"criteria": [{"filterType": 7, "value": ext_id}], "pageSize": 1}],
        "flags": 529,
    }
    status, data = http_json(
        MARKETPLACE, body, {"Accept": "application/json;api-version=7.1-preview.1"}
    )
    if status == 0 or status >= 400:
        raise RuntimeError(f"marketplace HTTP {status}")
    found = (data.get("results") or [{}])[0].get("extensions") or []
    if not found:
        return None
    latest = found[0]["versions"][0]
    props = {p["key"]: p["value"] for p in latest.get("properties", [])}
    return {
        "version": latest["version"],
        "updated": latest["lastUpdated"],
        "engine": props.get("Microsoft.VisualStudio.Code.Engine"),
        "prerelease": props.get("Microsoft.VisualStudio.Code.PreRelease") == "true",
    }

This endpoint is not a documented contract for third-party tools, and Microsoft can change it without notice. Treat it as a convenience for an audit and not as a dependency for production traffic. Raising an error on any non-success status, as the code does, makes breakage loud instead of silently reporting everything as missing.

Step 4 adds the Open VSX side. This API is documented and friendly: a GET on /api/{namespace}/{name}/latest returns the newest build, and an unknown extension returns a JSON body with an error field. The function also normalizes the engine requirement from the engines.vscode field. Append it, together with a search helper that step 10 will use:



def openvsx_lookup(ext_id):
    namespace, _, name = ext_id.partition(".")
    status, data = http_json(f"{OPENVSX}/{namespace}/{name}/latest")
    if status == 0 or status >= 500:
        raise RuntimeError(f"open-vsx HTTP {status}")
    if status == 404 or "error" in data:
        return None
    return {
        "version": data["version"],
        "updated": data["timestamp"],
        "engine": (data.get("engines") or {}).get("vscode"),
        "prerelease": bool(data.get("preRelease")),
    }


def openvsx_suggest(ext_id, size=3):
    term = ext_id.partition(".")[2].replace("-", " ")
    query = urllib.parse.urlencode(
        {"query": term, "size": size, "sortBy": "downloadCount", "sortOrder": "desc"}
    )
    status, data = http_json(f"{OPENVSX}/-/search?{query}")
    if status != 200:
        return []
    return [f"{e['namespace']}.{e['name']}" for e in data.get("extensions", [])]

You can sanity-check the client from a Python shell before moving on. Calling openvsx_lookup("esbenp.prettier-vscode") should return a dictionary with a version and a timestamp, and openvsx_lookup("ms-vscode.cpptools") should return None. When I ran it on October 24, 2026, the first call returned version 12.4.0 and the second returned nothing, so the IDs are good test fixtures for your own run, though registry contents can change at any time.

Steps 5 and 6: Read Extension IDs From Config Files and Classify Each One

Step 5 handles inputs. A devcontainer.json file is JSON with comments, and the spec allows trailing commas, so json.loads alone will choke on a normal real-world file. The strip_jsonc function walks the text one character at a time, remembers whether it sits inside a string, and drops line comments and block comments only when it does not. A regular expression then removes trailing commas. The Dev Container metadata reference documents where the extension list lives, and the reader below checks both the current customizations.vscode.extensions location and the older top-level extensions property.



def strip_jsonc(text):
    out, i, n, in_str = [], 0, len(text), False
    while i < n:
        ch = text[i]
        if in_str:
            out.append(ch)
            if ch == "\\" and i + 1 < n:
                out.append(text[i + 1])
                i += 1
            elif ch == '"':
                in_str = False
        elif ch == '"':
            in_str = True
            out.append(ch)
        elif text.startswith("//", i):
            while i < n and text[i] != "\n":
                i += 1
            continue
        elif text.startswith("/*", i):
            end = text.find("*/", i + 2)
            i = n if end < 0 else end + 2
            continue
        else:
            out.append(ch)
        i += 1
    return re.sub(r",(\s*[}\]])", r"\1", "".join(out))


def read_list(path):
    found = {}
    with open(path, encoding="utf-8") as fh:
        for line in fh:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            ext_id, _, version = line.partition("@")
            if ID_RE.match(ext_id):
                found[ext_id.lower()] = version or None
    return found


def read_devcontainer(path):
    with open(path, encoding="utf-8") as fh:
        data = json.loads(strip_jsonc(fh.read()))
    ids = list(data.get("extensions", []))
    ids += data.get("customizations", {}).get("vscode", {}).get("extensions", [])
    return {i.split("@")[0].lower(): None for i in ids if ID_RE.match(i.split("@")[0])}


def read_recommendations(path):
    with open(path, encoding="utf-8") as fh:
        data = json.loads(strip_jsonc(fh.read()))
    recs = data.get("recommendations", [])
    return {i.lower(): None for i in recs if ID_RE.match(i)}

Dev container Features can contribute extensions too, and those never appear in your devcontainer.json. If a Feature in your file installs tooling, check its own metadata separately. The VS Code dev container setup for AI agents covers one example of a container that carries extra editor-side pieces.

Step 6 is the decision logic. Comparing version strings across registries is a trap, since publishers use different schemes and Open VSX sometimes carries a pre-release or a platform build under the same number. The audit compares timestamps instead. If the version strings match, the verdict is OK. If they differ but the Open VSX build is within your lag limit (14 days by default), the verdict is CLOSE. Past that, it is LAGGING. A missing Open VSX entry is MISSING, and an ID that only exists on Open VSX is OVSX_ONLY. The engine check runs before the version comparison, so an Open VSX build that needs a newer VS Code base than your Cursor build ships gets flagged as ENGINE.



def version_tuple(spec):
    nums = re.findall(r"\d+", spec or "")[:3]
    return tuple(int(n) for n in nums) + (0,) * (3 - len(nums))


def engine_ok(required, editor_version):
    return version_tuple(editor_version) >= version_tuple(required)


def classify(ms, ov, lag_limit=14, editor_engine=None):
    if ms is None and ov is None:
        return "UNKNOWN", None
    if ov is None:
        return "MISSING", None
    if ms is None:
        return "OVSX_ONLY", None
    lag = (parse_ts(ms["updated"]) - parse_ts(ov["updated"])).days
    if editor_engine and ov.get("engine") and not engine_ok(ov["engine"], editor_engine):
        return "ENGINE", lag
    if ms["version"] == ov["version"]:
        return "OK", lag
    return ("CLOSE" if lag <= lag_limit else "LAGGING"), lag

The version_tuple helper pulls the first three numbers out of a spec like ^1.101.0 and compares them as a tuple. It ignores caret and tilde semantics on purpose. For an audit, “the extension asks for at least this base version” is the question that matters.

Steps 7 to 9: Cache the Lookups, Print the Report and Run Your First Audit

Step 7 ties the clients together. The audit function fans lookups out over a small thread pool, three workers by default, and caches both registry answers in a JSON file for six hours. Open VSX’s managed registry pricing page documents sustained request ceilings per tier, with its free tier at up to 3 requests per second, so keeping concurrency low and answers cached is the polite default even though an individual audit is tiny. Any lookup that raises an error becomes a row with status ERROR instead of killing the whole run.



def load_cache():
    try:
        with open(CACHE_PATH, encoding="utf-8") as fh:
            return json.load(fh)
    except (OSError, ValueError):
        return {}


def save_cache(cache):
    os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True)
    with open(CACHE_PATH, "w", encoding="utf-8") as fh:
        json.dump(cache, fh)


def cached(cache, kind, ext_id, fetch):
    key = f"{kind}:{ext_id}"
    hit = cache.get(key)
    if hit and time.time() - hit["t"] < CACHE_TTL:
        return hit["v"]
    value = fetch(ext_id)
    cache[key] = {"t": time.time(), "v": value}
    return value


def audit(extensions, lag_limit, editor_engine, workers, use_cache):
    cache = load_cache() if use_cache else {}

    def one(item):
        ext_id, installed = item
        row = {"id": ext_id, "installed": installed, "status": "ERROR", "lag_days": None,
               "ms_version": "", "ms_updated": "", "ov_version": "", "ov_updated": "",
               "ov_engine": ""}
        try:
            ms = cached(cache, "ms", ext_id, marketplace_lookup)
            ov = cached(cache, "ov", ext_id, openvsx_lookup)
        except RuntimeError as err:
            row["note"] = str(err)
            return row
        row["status"], row["lag_days"] = classify(ms, ov, lag_limit, editor_engine)
        if ms:
            row["ms_version"], row["ms_updated"] = ms["version"], ms["updated"][:10]
        if ov:
            row["ov_version"], row["ov_updated"] = ov["version"], ov["updated"][:10]
            row["ov_engine"] = ov.get("engine") or ""
        return row

    with cf.ThreadPoolExecutor(max_workers=workers) as pool:
        rows = list(pool.map(one, sorted(extensions.items())))
    if use_cache:
        save_cache(cache)
    return rows

Step 8 writes the report layer. The table sorts the worst statuses to the top, because that is what a person scanning a terminal needs first, and the CSV writer produces a file you can open in a spreadsheet or attach to a CI run.



ORDER = ["MISSING", "ENGINE", "LAGGING", "ERROR", "UNKNOWN", "CLOSE", "OVSX_ONLY", "ALLOWED", "OK"]


def print_table(rows):
    rows = sorted(rows, key=lambda r: (ORDER.index(r["status"]), r["id"]))
    print(f"{'STATUS':<10} {'EXTENSION':<40} {'MARKETPLACE':<22} {'OPEN VSX':<18} LAG")
    for r in rows:
        lag = "" if r["lag_days"] is None else f"{r['lag_days']}d"
        print(f"{r['status']:<10} {r['id']:<40} {r['ms_version'][:21]:<22} "
              f"{r['ov_version'][:17]:<18} {lag}")
        if r["status"] == "ERROR":
            print(f"           note: {r.get('note', 'unknown error')}")
    counts = {s: sum(r["status"] == s for r in rows) for s in ORDER}
    print("\n" + "  ".join(f"{s}={n}" for s, n in counts.items() if n))


def write_csv(rows, path):
    fields = ["id", "status", "installed", "ms_version", "ms_updated",
              "ov_version", "ov_updated", "lag_days", "ov_engine"]
    with open(path, "w", newline="", encoding="utf-8") as fh:
        writer = csv.DictWriter(fh, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)

Step 9 finishes the CLI. The audit command accepts any mix of list files, devcontainer.json files and recommendation files, merges the IDs, runs the lookups, and returns a non-zero exit code when a status you listed in --fail-on shows up. The diff command compares two extension dumps. Append this last piece to complete the file:



def cmd_audit(args):
    extensions = {}
    for path in args.list or []:
        extensions.update(read_list(path))
    for path in args.devcontainer or []:
        extensions.update(read_devcontainer(path))
    for path in args.recommendations or []:
        extensions.update(read_recommendations(path))
    if not extensions:
        sys.exit("no extension IDs found in the inputs")
    rows = audit(extensions, args.lag_days, args.editor_engine, args.workers, not args.no_cache)
    if args.allowlist:
        allowed = set(read_list(args.allowlist))
        for r in rows:
            if r["id"] in allowed and r["status"] in {"MISSING", "ENGINE", "LAGGING"}:
                r["status"] = "ALLOWED"
    print_table(rows)
    if args.csv:
        write_csv(rows, args.csv)
    if args.json:
        with open(args.json, "w", encoding="utf-8") as fh:
            json.dump(rows, fh, indent=2)
    if args.suggest:
        for r in rows:
            if r["status"] == "MISSING":
                print(f"suggest {r['id']}: {', '.join(openvsx_suggest(r['id'])) or 'nothing found'}")
    if args.emit_install:
        safe = {"OK", "CLOSE", "OVSX_ONLY"}
        with open(args.emit_install, "w", encoding="utf-8") as fh:
            fh.write("#!/bin/sh\nset -e\n")
            for r in rows:
                if r["status"] in safe:
                    fh.write(f"cursor --install-extension {r['id']}\n")
        os.chmod(args.emit_install, 0o755)
    bad = set(args.fail_on.split(",")) if args.fail_on else set()
    return 1 if any(r["status"] in bad for r in rows) else 0


def cmd_diff(args):
    left, right = read_list(args.vscode), read_list(args.cursor)
    only_left = sorted(set(left) - set(right))
    only_right = sorted(set(right) - set(left))
    drift = sorted(i for i in set(left) & set(right) if left[i] and right[i] and left[i] != right[i])
    print(f"in VS Code but not Cursor ({len(only_left)}):")
    print("\n".join(f"  {i}" for i in only_left) or "  none")
    print(f"in Cursor but not VS Code ({len(only_right)}):")
    print("\n".join(f"  {i}" for i in only_right) or "  none")
    print(f"version drift ({len(drift)}):")
    print("\n".join(f"  {i}  vscode={left[i]}  cursor={right[i]}" for i in drift) or "  none")
    return 1 if only_left else 0


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    sub = parser.add_subparsers(dest="cmd", required=True)
    a = sub.add_parser("audit", help="check IDs against both registries")
    a.add_argument("--list", action="append", help="output of code --list-extensions [--show-versions]")
    a.add_argument("--devcontainer", action="append", help="path to devcontainer.json")
    a.add_argument("--recommendations", action="append", help="path to .vscode/extensions.json")
    a.add_argument("--lag-days", type=int, default=14)
    a.add_argument("--editor-engine", help="VS Code base version of your Cursor build, e.g. 1.105.0")
    a.add_argument("--workers", type=int, default=3)
    a.add_argument("--no-cache", action="store_true")
    a.add_argument("--csv")
    a.add_argument("--json")
    a.add_argument("--suggest", action="store_true")
    a.add_argument("--emit-install")
    a.add_argument("--allowlist", help="IDs whose gaps your team has accepted")
    a.add_argument("--fail-on", default="", help="comma list, e.g. MISSING,ENGINE")
    a.set_defaults(func=cmd_audit)
    d = sub.add_parser("diff", help="compare two --list-extensions dumps")
    d.add_argument("vscode")
    d.add_argument("cursor")
    d.set_defaults(func=cmd_diff)
    args = parser.parse_args()
    sys.exit(args.func(args))


if __name__ == "__main__":
    main()

Create a sample dev container file so there is something to audit besides the plain list. Note the comments and the trailing comma, both of which are legal in this format:

{
  // Node 22 service container
  "name": "api-service",
  "image": "mcr.microsoft.com/devcontainers/typescript-node:22",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "ms-azuretools.vscode-docker", /* needs the Docker socket */
        "ms-vscode-remote.remote-containers",
        "bradlc.vscode-tailwindcss",
      ]
    }
  }
}

Run it

With both inputs in place, run the first audit. I disabled the cache so the output reflects live registry state:

python3 extparity.py audit \
  --list fixtures/vscode-extensions.txt \
  --devcontainer fixtures/devcontainer.json \
  --csv parity.csv --suggest --no-cache

This is the real output from my run on October 24, 2026. Your numbers will differ as publishers ship new builds:

STATUS     EXTENSION                                MARKETPLACE            OPEN VSX           LAG
MISSING    ms-python.vscode-pylance                 2026.4.101                                
MISSING    ms-vscode-remote.remote-containers       0.470.0                                   
MISSING    ms-vscode-remote.remote-ssh              0.129.2026100915                          
MISSING    ms-vscode.cpptools                       1.35.4                                    
LAGGING    ms-python.python                         2026.11.2026102301     2026.4.0           224d
CLOSE      golang.go                                0.59.0                 0.58.0             1d
OK         bradlc.vscode-tailwindcss                0.16.0                 0.16.0             -3d
OK         dbaeumer.vscode-eslint                   3.0.34                 3.0.34             -1d
OK         eamodio.gitlens                          2026.10.200514         2026.10.200514     0d
OK         esbenp.prettier-vscode                   12.4.0                 12.4.0             0d
OK         ms-azuretools.vscode-docker              2.0.0                  2.0.0              -1d
OK         redhat.java                              1.59.2026102408        1.59.2026102408    0d

MISSING=4  LAGGING=1  CLOSE=1  OK=6
suggest ms-python.vscode-pylance: ms-python.python, ms-python.vscode-python-envs, devsense.composer-php-vscode
suggest ms-vscode-remote.remote-containers: ms-azuretools.vscode-containers, ms-azuretools.vscode-docker, ms-vscode.js-debug-companion
suggest ms-vscode-remote.remote-ssh: ms-vscode.js-debug-companion, jmxgrog.vscode-nuget-package-manager, eclipse-cdt.cdt-gdb-vscode
suggest ms-vscode.cpptools: ms-vscode.cpptools-themes, Codeium.windsurf-cpptools, embedd-team.cpptools-proxy-patcher

Twelve unique IDs came out of the two inputs, since three of them appear in both files. Four are MISSING from Open VSX, one is LAGGING, one is CLOSE and six are OK. The Python extension is the striking row. The Marketplace build is far newer than the Open VSX build, and the LAG column shows that gap in days. A teammate on VS Code and a teammate on Cursor are not running the same Python tooling, even though both have the Python extension. If you also keep editor settings aligned with the settings sync guide for both editors, add this audit next to it, because synced settings cannot fix an extension that never installed.

Read the statuses

StatusWhat it meansWhat to do
OKSame version on both registriesNothing, install in Cursor
CLOSEOpen VSX is behind by less than your lag limitInstall, and re-check next week
LAGGINGOpen VSX is behind by more than the limitDecide whether the missing releases matter
ENGINEOpen VSX build needs a newer VS Code base than your editorUpdate Cursor or pin an older extension version
MISSINGNo Open VSX entry for this IDFind an alternative or accept the gap
OVSX_ONLYExists on Open VSX but not the MarketplaceCheck the publisher before relying on it
ALLOWEDA gap your team accepted in the allowlistReview the allowlist each quarter
ERRORA lookup failedRe-run, then check network and rate limits

The CSV head, handy for pivot tables, looks like this:

id,status,installed,ms_version,ms_updated,ov_version,ov_updated,lag_days,ov_engine
bradlc.vscode-tailwindcss,OK,,0.16.0,2026-07-17,0.16.0,2026-07-20,-3,^1.67.0
dbaeumer.vscode-eslint,OK,,3.0.34,2026-07-16,3.0.34,2026-07-17,-1,^1.90.0
eamodio.gitlens,OK,,2026.10.200514,2026-10-20,2026.10.200514,2026-10-20,0,^1.101.0

Step 10: Find Alternatives and Generate a Cursor Install Script

The --suggest flag already ran in the previous step, and the last four lines of the output show what it found. Treat those lines as search hits, nothing more. The helper searches Open VSX by the name part of the missing ID, sorted by download count, so a query for vscode-pylance returns the Python extension and a PHP tool rather than a Pylance replacement. For ms-vscode.cpptools the results were even less useful. A manual search for “clangd” on Open VSX returned llvm-vs-code-extensions.vscode-clangd as the top result, which is a real alternative for C and C++ language support, but only a human can judge that.

Cross-check anything the tool suggests. A name that looks right can still belong to a stranger. Socket’s August 2026 write-up on Open VSX and a fake-extension campaign is a reminder that an ID existing on a registry does not make it trustworthy, so check the publisher, the repository link and the download count before you swap anything into a shared config.

The second half of this step turns the safe rows into an install script. Run:

python3 extparity.py audit \
  --list fixtures/vscode-extensions.txt \
  --emit-install install-cursor.sh --no-cache
cat install-cursor.sh

The script contains one cursor --install-extension line for each OK, CLOSE or OVSX_ONLY row and nothing else. MISSING, LAGGING and ENGINE rows stay out on purpose, because installing them blindly is exactly how you end up with a half-working setup. From my sample the script held six install lines. Review it, then run it with sh install-cursor.sh.

The engine check deserves its own demo. Look up the VS Code base version of your Cursor build in its About dialog, then pass it with --editor-engine. To show the effect, I passed a deliberately old value:

STATUS     EXTENSION                                MARKETPLACE            OPEN VSX           LAG
MISSING    ms-python.vscode-pylance                 2026.4.101                                
MISSING    ms-vscode-remote.remote-ssh              0.129.2026100915                          
MISSING    ms-vscode.cpptools                       1.35.4                                    
ENGINE     eamodio.gitlens                          2026.10.200514         2026.10.200514     0d
ENGINE     esbenp.prettier-vscode                   12.4.0                 12.4.0             0d
ENGINE     golang.go                                0.59.0                 0.58.0             1d
ENGINE     ms-azuretools.vscode-docker              2.0.0                  2.0.0              -1d
ENGINE     ms-python.python                         2026.11.2026102301     2026.4.0           224d
OK         dbaeumer.vscode-eslint                   3.0.34                 3.0.34             -1d
OK         redhat.java                              1.59.2026102408        1.59.2026102408    0d

MISSING=3  ENGINE=5  OK=2

Several extensions that were fine a moment ago now show ENGINE, because their latest Open VSX builds ask for a newer base version than the one I passed. That value is a demonstration value, not a statement about any real Cursor build. Use your own number.

Step 11: Diff VS Code Against What Cursor Actually Installed

The registry audit tells you what should work. The diff command tells you what actually landed. Export the list from both editors after you run the install script:

code --list-extensions --show-versions > vscode-dump.txt
cursor --list-extensions --show-versions > cursor-dump.txt
python3 extparity.py diff vscode-dump.txt cursor-dump.txt

Here is the output for two small sample dumps I put in the fixtures folder:

in VS Code but not Cursor (1):
  ms-vscode.cpptools
in Cursor but not VS Code (1):
  eamodio.gitlens
version drift (1):
  dbaeumer.vscode-eslint  vscode=3.0.30  cursor=3.0.34

The command prints three groups, extensions only VS Code has, extensions only Cursor has, and extensions present in both with different versions. It returns exit code 1 whenever the first group is non-empty, so you can use it as a gate on onboarding scripts. In the sample, ms-vscode.cpptools exists only on the VS Code side, and dbaeumer.vscode-eslint sits at a different version in Cursor than in VS Code. Version drift is usually harmless, but it explains the odd works-on-my-machine report.

Pair this with the rest of your cross-editor setup. If you already keep rules and prompts in sync following the shared AI rules guide for one repo, add the diff command to the same onboarding script so a new hire gets matching rules and a matching extension set in one pass.

Steps 12 and 13: Add Unit Tests, an Allowlist and a CI Gate

Step 12 is the test file. Six tests cover the parts most likely to break: comment stripping, trailing commas, extraction from both dev container locations, every classification branch, the engine gate and the list reader. They run offline and finish in milliseconds, so they cost nothing to keep.

import os
import sys
import tempfile
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import extparity as ep  # noqa: E402


def stamp(day):
    return f"2026-10-{day:02d}T10:00:00.000Z"


class JsoncTests(unittest.TestCase):
    def test_comments_and_trailing_commas(self):
        text = '{"a": "x // not a comment", /* gone */ "b": [1, 2,],} // tail'
        self.assertEqual(ep.json.loads(ep.strip_jsonc(text)), {"a": "x // not a comment", "b": [1, 2]})

    def test_devcontainer_ids_from_both_locations(self):
        text = ('{"extensions": ["A.one"], "customizations": {"vscode": '
                '{"extensions": ["[email protected]", "bad id"]}}}')
        with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
            fh.write(text)
        self.assertEqual(set(ep.read_devcontainer(fh.name)), {"a.one", "b.two"})
        os.unlink(fh.name)


class ClassifyTests(unittest.TestCase):
    def test_statuses(self):
        ms = {"version": "2.0.0", "updated": stamp(20), "engine": "^1.90.0"}
        same = {"version": "2.0.0", "updated": stamp(19), "engine": "^1.90.0"}
        near = {"version": "1.9.0", "updated": stamp(15), "engine": "^1.90.0"}
        old = {"version": "1.0.0", "updated": stamp(1), "engine": "^1.90.0"}
        self.assertEqual(ep.classify(ms, same)[0], "OK")
        self.assertEqual(ep.classify(ms, near)[0], "CLOSE")
        self.assertEqual(ep.classify(ms, old)[0], "LAGGING")
        self.assertEqual(ep.classify(ms, None)[0], "MISSING")
        self.assertEqual(ep.classify(None, same)[0], "OVSX_ONLY")
        self.assertEqual(ep.classify(None, None)[0], "UNKNOWN")

    def test_engine_gate(self):
        ms = {"version": "2.0.0", "updated": stamp(20), "engine": "^1.99.0"}
        ov = {"version": "2.0.0", "updated": stamp(20), "engine": "^1.99.0"}
        self.assertEqual(ep.classify(ms, ov, editor_engine="1.98.2")[0], "ENGINE")
        self.assertEqual(ep.classify(ms, ov, editor_engine="1.105.0")[0], "OK")


class AllowlistTests(unittest.TestCase):
    def test_order_contains_allowed(self):
        self.assertIn("ALLOWED", ep.ORDER)


class ListTests(unittest.TestCase):
    def test_read_list_keeps_installed_version(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as fh:
            fh.write("# comment\[email protected]\n\nnot-an-id\n")
        self.assertEqual(ep.read_list(fh.name), {"ms-python.python": "2026.1.0"})
        os.unlink(fh.name)


if __name__ == "__main__":
    unittest.main()

Run them with python3 -m unittest discover -s tests -v. Here is the output I got:

test_order_contains_allowed (test_extparity.AllowlistTests.test_order_contains_allowed) ... ok
test_engine_gate (test_extparity.ClassifyTests.test_engine_gate) ... ok
test_statuses (test_extparity.ClassifyTests.test_statuses) ... ok
test_comments_and_trailing_commas (test_extparity.JsoncTests.test_comments_and_trailing_commas) ... ok
test_devcontainer_ids_from_both_locations (test_extparity.JsoncTests.test_devcontainer_ids_from_both_locations) ... ok
test_read_list_keeps_installed_version (test_extparity.ListTests.test_read_list_keeps_installed_version) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.009s

OK

Step 13 makes the tool useful for a team. A CI job that fails whenever any Microsoft extension is missing from Open VSX would be permanently red, which trains everyone to ignore it. The allowlist solves that. List the IDs whose gaps your team has accepted, one per line, and the audit downgrades matching MISSING, ENGINE and LAGGING rows to ALLOWED. New gaps still fail the build.

# acknowledged gaps
ms-vscode-remote.remote-containers
ms-vscode-remote.remote-ssh
ms-vscode.cpptools
ms-python.vscode-pylance
ms-python.python

Run the audit with the allowlist and a failure policy:

python3 extparity.py audit \
  --list fixtures/vscode-extensions.txt \
  --devcontainer fixtures/devcontainer.json \
  --allowlist fixtures/parity-allowlist.txt \
  --fail-on MISSING,ENGINE,LAGGING --no-cache
STATUS     EXTENSION                                MARKETPLACE            OPEN VSX           LAG
CLOSE      golang.go                                0.59.0                 0.58.0             1d
ALLOWED    ms-python.python                         2026.11.2026102301     2026.4.0           224d
ALLOWED    ms-python.vscode-pylance                 2026.4.101                                
ALLOWED    ms-vscode-remote.remote-containers       0.470.0                                   
ALLOWED    ms-vscode-remote.remote-ssh              0.129.2026100915                          
ALLOWED    ms-vscode.cpptools                       1.35.4                                    
OK         bradlc.vscode-tailwindcss                0.16.0                 0.16.0             -3d
OK         dbaeumer.vscode-eslint                   3.0.34                 3.0.34             -1d
OK         eamodio.gitlens                          2026.10.200514         2026.10.200514     0d
OK         esbenp.prettier-vscode                   12.4.0                 12.4.0             0d
OK         ms-azuretools.vscode-docker              2.0.0                  2.0.0              -1d
OK         redhat.java                              1.59.2026102408        1.59.2026102408    0d

CLOSE=1  ALLOWED=5  OK=6

Exit code 0 means every gap in the sample is either acknowledged or small, while the diff command in step 11 exits with 1 when VS Code has extensions that Cursor lacks. If someone adds a new Microsoft-only extension to devcontainer.json next month, the build turns red and a human decides whether to allowlist it. Finally, drop this workflow into .github/workflows/ext-parity.yml. It runs on pull requests that touch the extension files and every Monday at 06:00 UTC:

name: extension-parity
on:
  schedule:
    - cron: "0 6 * * 1"
  pull_request:
    paths:
      - ".devcontainer/devcontainer.json"
      - ".vscode/extensions.json"
      - "parity-allowlist.txt"
  workflow_dispatch:

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Run unit tests
        run: python3 -m unittest discover -s tests
      - name: Audit extension IDs against Open VSX
        run: >
          python3 extparity.py audit
          --devcontainer .devcontainer/devcontainer.json
          --recommendations .vscode/extensions.json
          --allowlist parity-allowlist.txt
          --fail-on MISSING,ENGINE,LAGGING
          --csv parity.csv --no-cache
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: parity-report
          path: parity.csv

Pin the action versions to whatever majors your organization currently approves. If you want the audit to sit next to a performance comparison, the Cursor vs VS Code benchmark on a real codebase uses a similar harness style.

Deciding What to Do With Each Gap

A report only helps if it ends in a decision. After a few runs against different projects, the gaps fall into a handful of categories, and each category has a sensible default response. The sample run from October 24, 2026 shows all of them at once.

CategoryExample from the sample runDefault response
Language support from Microsoftms-python.python lagging by 224 days, ms-vscode.cpptools missingSearch for an open alternative, test it on one repository, then allowlist the original with a review date
Remote and container toolingms-vscode-remote.remote-ssh and ms-vscode-remote.remote-containers missingConfirm that Cursor’s own remote workflow opens your project, then allowlist the Microsoft IDs
Formatters and lintersdbaeumer.vscode-eslint and esbenp.prettier-vscode matchingNothing to do, let the weekly CI run watch them
Small version laggolang.go one day behindInstall it and ignore the gap, since the next publish usually closes it
Container and cloud helpersms-azuretools.vscode-docker matchingNothing to do

Work through new gaps with a five-minute triage. Keep the order, because each step is cheaper than the one after it.

  1. Check whether the extension is purely cosmetic, such as a theme or an icon pack. If so, a gap costs you nothing and you can allowlist it immediately.
  2. Check whether Cursor or the dev container already bundles the function. Remote access and container support are the usual examples.
  3. Search Open VSX by function rather than by name, and sort by downloads. Read the publisher page and the linked repository before you trust a hit.
  4. Test the candidate on one repository for a day. A language extension that installs cleanly can still behave differently, so run your normal build and test commands.
  5. Record the decision. Add the original ID to the allowlist and write the reason and the review date in your team notes, because the allowlist file itself ignores everything after the ID.

That last point matters more than it sounds. The reader splits each line at the at sign and treats anything starting with a hash as a comment, so a trailing note on the same line would be parsed as part of the ID and rejected. Put your comments on their own lines above each entry instead. A team that writes down why a gap was accepted can revisit it in three months. A team that only adds IDs ends up with an allowlist nobody dares to trim.

Escalate rather than allowlist when a gap involves trust. An OVSX_ONLY row for an extension your team has never used, or a MISSING row that a teammate wants to fill with an unreviewed package, deserves a second pair of eyes. The cost of a half-day security review is small next to the cost of an extension that can read your repositories and your CI secrets. Language servers and formatters run with the same file access as the editor, so the permissions question is real even for tools that look harmless.

Finally, decide who owns the report. In practice the best owner is whoever maintains the dev container, since that person already approves changes to devcontainer.json. Make the weekly CI failure open an issue assigned to them, and the audit stops being a script and becomes a habit.

Common Pitfalls

Seven mistakes account for most wrong audit results. Each one has burned someone, and most are avoidable once you know about them.

  1. Trusting version strings across registries. A Marketplace build and an Open VSX build of the same extension can look comparable, but publishers follow different schemes, and some release pre-release builds under the same line. When I probed redhat.java, the Open VSX latest build carried a pre-release flag. Timestamps give a fairer lag measure, and the audit uses them for that reason.
  2. Reading MISSING as “broken.” A missing Microsoft remote extension does not mean remote work fails in Cursor, since the editor reportedly ships its own replacements. MISSING means “no drop-in on Open VSX.” It does not mean “no solution.”
  3. Sideloading Microsoft-licensed packages to close a gap. It is technically possible to install a .vsix file you downloaded elsewhere. Check the extension’s license and the Marketplace terms first. The tool deliberately never generates download links for this reason.
  4. Auditing the host instead of the container. code --list-extensions on your laptop says nothing about what runs inside a dev container. Run the export from the container terminal, or audit the devcontainer.json file directly, as this tutorial does.
  5. Forgetting that IDs are case-insensitive. The reader lowercases every ID, which keeps Redhat.Java and redhat.java from showing up as two rows. If you add your own parsers, do the same.
  6. Treating an existing ID as a trusted one. The audit confirms that an ID resolves. It does not confirm that the publisher is who you think it is. Check the repository link, the verification flag and the download count for anything new, especially anything that appears only on Open VSX. If you author extensions yourself, the tutorial on building a VS Code AI extension that works in Cursor shows how to test against both editors.
  7. Hammering the registries in CI. A matrix job that runs the audit on twenty repositories at once will trip rate limits. Keep the default worker count, keep the cache on locally, and stagger scheduled runs.

Troubleshooting

These are the failures you are most likely to hit, with the quickest fix for each.

SymptomLikely causeFix
command not found: cursorShell command never installedRun the editor’s “Install command in PATH” action, then open a new terminal
no extension IDs found in the inputsEmpty file, wrong path, or lines that fail the ID patternOpen the file, confirm each line looks like publisher.name, and strip any platform suffixes
json.decoder.JSONDecodeError on devcontainer.jsonA byte order mark at the top of the fileChange the open calls to encoding="utf-8-sig"
Every row shows ERROR with the note marketplace HTTP 0No route to the registries, often a proxy or TLS inspectionSet HTTPS_PROXY and SSL_CERT_FILE in the shell, since urllib reads them
HTTP 429 in the ERROR noteToo many requests too fastLower --workers, keep the cache on, and rerun after a minute
Marketplace column empty, Open VSX filledThe ID is misspelled or the extension is unpublished therePaste the ID into the Marketplace search and confirm the exact spelling
Row says OK but Cursor refuses the installEngine mismatch, or a registry cache delayRe-run with --editor-engine set to your Cursor build’s VS Code base version
CI is red on every runMicrosoft-only extensions with no allowlistAdd the accepted IDs to the allowlist file and commit it
Results look staleThe six-hour cache is serving old answersPass --no-cache or delete ~/.cache/extparity.json
PermissionError writing the cache in CIRead-only home directory in a containerUse --no-cache in CI, which the sample workflow already does
LAG looks wrong for a platform-specific extensionRegistries hold different per-platform buildsCompare the targetPlatform field in the raw API response before acting

If none of these match, run the failing ID through curl against the Open VSX API directly. A raw response shows you whether the problem lives in the registry or in the script.

Advanced Tips

Once the basic audit works, five extensions make it considerably more useful.

Add trust signals to the report. The Open VSX response includes a verified flag and a publishedWithTrustedPublishing flag. Adding both as CSV columns takes four lines, and it lets a reviewer sort the OVSX_ONLY rows by trust first. When I probed redhat.java the verified flag was true, and the trusted-publishing flag was false.

Pin versions in the install script. Cursor’s CLI follows VS Code’s syntax, so id@version should work with --install-extension, and the emit step can write exact versions. That makes onboarding reproducible and gives you a diff in code review when someone bumps one.

Run the audit inside the container. With the Dev Containers CLI, devcontainer exec --workspace-folder . python3 extparity.py audit --list container-dump.txt audits what the container actually loaded. Pair it with a post-create command that dumps the list.

Audit recommendations too. Many repositories keep a .vscode/extensions.json file with a recommendations array that nobody has audited. The --recommendations flag handles it, and the CI workflow above already passes it.

Consider a managed registry for large teams. Organizations that integrate directly with Open VSX can buy a managed registry with paid tiers, while individuals and open-source projects use the free public registry. If your company routes hundreds of installs a day through one registry, read that pricing page before the rate limits find you. For a broader look at working in both editors, the Cursor AI tutorial covers the editor side of the story.

You can also feed the JSON output into a dashboard. The --json flag writes the same rows as the CSV, which makes weekly trend tracking a ten-minute job. Store one file per run, key it by date, and chart the count of MISSING and LAGGING rows over time. A flat line tells you your list is stable. A steady climb tells you the team keeps adding Microsoft-only extensions without checking, and that is a conversation worth having before the next onboarding. The lag column is also a useful early warning, since a publisher whose Open VSX builds slip from 3 days to 60 days has probably stopped pushing there.

The Complete Project at a Glance

By the end of step 13 your folder looks like this. Everything fits in one script of about 340 lines, plus fixtures and one workflow file:

extparity/
  extparity.py
  Makefile
  fixtures/
    vscode-extensions.txt
    devcontainer.json
    parity-allowlist.txt
    vscode-dump.txt
    cursor-dump.txt
  tests/
    test_extparity.py
  .github/workflows/
    ext-parity.yml

The Makefile wraps the three commands you will run most often. Save it as Makefile in the project root and use make test, make audit and make diff:

PY ?= python3

test:
	$(PY) -m unittest discover -s tests -v

audit:
	$(PY) extparity.py audit --list fixtures/vscode-extensions.txt \
	  --devcontainer fixtures/devcontainer.json \
	  --allowlist fixtures/parity-allowlist.txt \
	  --csv parity.csv --fail-on MISSING,ENGINE,LAGGING

diff:
	$(PY) extparity.py diff fixtures/vscode-dump.txt fixtures/cursor-dump.txt

A quick end-to-end check proves the project works. Run make test first and expect six passing tests. Then run make audit, which exits 0 against the sample allowlist. Then temporarily delete one line from fixtures/parity-allowlist.txt and run it again. Exit code 1 means the gate catches a new gap, which is the whole point of the project. For a wider view of where this fits among other tools, the software category collects the related editor and tooling guides.

I did not benchmark editor speed here, and none of the output above measures performance. Everything printed in this article came from live registry queries and a local test run on October 24, 2026. Registry contents move daily, so treat the sample rows as a snapshot and trust your own run over mine.

Frequently Asked Questions

Does Cursor use the VS Code Marketplace?

No. Cursor’s help page says it uses the Open VSX extension registry, with many popular VS Code extensions available but not all of them listed or identical in behavior. VS Code uses Microsoft’s marketplace. That split is the reason this audit exists.

Why is an extension I use in VS Code missing in Cursor?

The publisher has not pushed that extension to Open VSX, or has pushed an older build. In my October 24, 2026 run, Microsoft’s C and C++, Pylance and Remote SSH extensions had no Open VSX entry at all. Check whether Cursor ships its own replacement before you assume you lost a feature.

Can I install a VSIX file manually instead?

Cursor accepts --install-extension path/to/file.vsix. Whether you are allowed to use a given package that way depends on its license and on the terms of the registry it came from. The audit tool never produces download links for that reason, so read the terms first.

Does devcontainer.json work the same in Cursor and VS Code?

Microsoft documents the file for VS Code. A third-party report says Cursor ships its own Dev Containers extension, so the file should open there, but the extension list inside it is still resolved against Open VSX. Test your own file and run the audit on it.

How often should I run the audit?

Weekly in CI is enough for most teams, plus on any pull request that changes devcontainer.json or the recommendations file. Registry contents change daily, but a gap that matters rarely appears and disappears within a week.

Is Open VSX free to use?

The public Open VSX Registry is free and open for publishing, search and downloads, and it is governed by the Eclipse Foundation. A separate managed registry offers paid tiers for organizations that integrate directly with it.

Will this work for VSCodium or other Open VSX editors?

It should, since those editors read from the same registry, but I only ran the tool against lists exported from VS Code and queries to the two registries. Test it with your own editor’s list before you rely on the result.

Sofia Lindström

Sofia Lindström

Editor-in-Chief

Sofia Lindström is the Editor-in-Chief at Tech Insider, where she leads editorial strategy and oversees coverage across AI, cybersecurity, and enterprise technology. With over a decade in Swedish tech journalism, she previously served as technology editor at Dagens Industri and covered the Nordic startup ecosystem for Breakit. Sofia holds an MSc in Media Technology from KTH Royal Institute of Technology and is a frequent speaker at Web Summit and Slush. She is passionate about making complex technology accessible to business leaders.

View all articles