Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aont/c7905f503e9f57530537019e2e53c1f2 to your computer and use it in GitHub Desktop.

Select an option

Save aont/c7905f503e9f57530537019e2e53c1f2 to your computer and use it in GitHub Desktop.

Two Handy Python Tools to Inspect and Clean VS Code Workspaces

If you use Visual Studio Code a lot, the workspaceStorage folder can fill up with old entries. The two scripts below help you (1) list everything that’s there and (2) safely delete one workspace’s metadata by its ID.


What the tools do

1) List all workspace IDs and URIs (cross-platform)

  • Scans the standard user-data roots for VS Code / Code – Insiders / Code – OSS on Windows, macOS, and Linux.

  • Reads each workspaceStorage/<id>/workspace.json.

  • Extracts the folder URI from one of:

    • folder
    • configuration.folder
    • workspace.folder
  • Prints a clean table with: Flavor, WorkspaceID, URI, StorePath.

  • Optional --filter does a case-insensitive substring match on the raw URI (no decoding or normalization).

  • Optional --csv PATH writes a CSV in UTF-8 with BOM so Excel opens it cleanly.

Intentional behavior: URIs are kept as-is (e.g., file:///c%3A/...). That makes filtering predictable and matches how VS Code stores keys elsewhere.

2) Remove one workspace by ID (Windows only)

  • Targets %APPDATA%\Code\User\... (the stable “Code” flavor on Windows).
  • Looks up the folder URI from workspaceStorage\<wsid>\workspace.json.
  • In %APPDATA%\Code\User\globalStorage\storage.json, it removes the exact-string key from profileAssociations.workspaces that matches that URI.
  • Backs up storage.json to storage.json.bak (or .bak.N).
  • Deletes the directory %APPDATA%\Code\User\workspaceStorage\<wsid>.
  • Supports --dry-run to preview changes without touching files.

Note: The deletion compares strings exactly; there’s no URI → path conversion. That’s on purpose to avoid mismatches.


Requirements

  • Python 3.10+ (for modern type hints)
  • No third-party packages; standard library only.

Quick start

A) List everything (Win/macOS/Linux)

# Show all workspaces in a table
python list_workspaces.py

# Narrow down by raw-URI substring (case-insensitive)
python list_workspaces.py --filter "file:///c%3A/Users/me/Projects"

# Save for Excel (UTF-8 with BOM)
python list_workspaces.py --csv workspaces.csv

Example table (trimmed):

+=================+=========================+==========================================+======================================+
| Flavor          | WorkspaceID             | URI                                      | StorePath                            |
+=================+=========================+==========================================+======================================+
| Code            | 1a2b3c...               | file:///c%3A/Users/me/Projects/foo       | C:\Users\me\AppData\Roaming\Code\...|
+=================+=========================+==========================================+======================================+

B) Delete one workspace (Windows)

  1. Find the WorkspaceID using the list script (or from the folder name under %APPDATA%\Code\User\workspaceStorage).

  2. Do a dry run first:

python remove_workspace.py --wsid 1a2b3c... --dry-run
  1. Apply if it looks right:
python remove_workspace.py --wsid 1a2b3c...

You’ll see:

  • Target paths (workspace.json, storage.json)
  • The folder URI detected
  • Whether an entry was removed from profileAssociations.workspaces
  • Confirmation that %APPDATA%\Code\User\workspaceStorage\<wsid> was deleted

Safety notes & tips

  • Close VS Code before running the removal script.
  • Always start with --dry-run.
  • A backup of storage.json is created automatically.
  • If storage.json doesn’t exist, the script simply skips that step.
  • If you see “workspace.json not found”, double-check the ID or whether it belongs to another flavor (e.g., Insiders/OSS).
  • The Windows removal script currently touches only the stable Code flavor; you can adapt it to Insiders/OSS by tweaking the base path.

Why keep the URI unchanged?

VS Code often stores workspace keys as percent-encoded URIs (e.g., file:///c%3A/...). By treating that string as the source of truth, the tools avoid accidental mismatches that can happen when converting URIs to local paths.


Summary

  • list_workspaces.py → Cross-platform inventory with optional CSV (Excel-friendly).
  • remove_workspace.py → Windows cleanup by ID with dry-run and backups.

They’re small, dependable utilities to keep your VS Code metadata tidy without guesswork.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import csv
import json
import os
import sys
from pathlib import Path
# Candidate user-data roots for various VS Code flavors
def candidate_roots() -> list[Path]:
roots: list[Path] = []
home = Path.home()
if sys.platform.startswith("win"):
appdata = os.environ.get("APPDATA")
if appdata:
base = Path(appdata)
roots += [
base / "Code",
base / "Code - Insiders",
base / "Code - OSS",
]
elif sys.platform == "darwin":
base = home / "Library" / "Application Support"
roots += [
base / "Code",
base / "Code - Insiders",
base / "Code - OSS",
]
else:
# Linux / others
config = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config"))
roots += [
config / "Code",
config / "Code - Insiders",
config / "Code - OSS",
]
return roots
def ws_roots() -> list[tuple[str, Path]]:
out: list[tuple[str, Path]] = []
for base in candidate_roots():
ws = base / "User" / "workspaceStorage"
if ws.is_dir():
out.append((base.name, ws))
return out
def read_workspace_json(p: Path) -> dict | None:
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return None
def extract_folder_uri(j: dict) -> str | None:
# Retrieve in the same order as the PowerShell version
return j.get("folder") or (j.get("configuration") or {}).get("folder") or (j.get("workspace") or {}).get("folder")
def collect_all(filter_substr: str | None = None) -> list[dict]:
rows: list[dict] = []
for flavor, root in ws_roots():
for sub in root.iterdir():
if not sub.is_dir():
continue
ws_json = sub / "workspace.json"
if not ws_json.is_file():
continue
j = read_workspace_json(ws_json)
if not j:
continue
uri = extract_folder_uri(j)
if not uri:
continue
# Filter: substring match against the URI only (no normalization/conversion)
if filter_substr and filter_substr.lower() not in uri.lower():
continue
rows.append({
"Flavor": flavor, # Code / Code - Insiders / Code - OSS
"WorkspaceID": sub.name, # Directory name
"URI": uri, # Keep as-is from JSON
"StorePath": str(sub), # Full path to workspaceStorage/<id>
})
# Sort for stability (Flavor → URI → WorkspaceID)
rows.sort(key=lambda r: (r["Flavor"], r["URI"], r["WorkspaceID"]))
return rows
def print_table(rows: list[dict]):
if not rows:
print("No entries found.")
return
headers = ["Flavor", "WorkspaceID", "URI", "StorePath"]
# Simple width calculation
widths = {h: len(h) for h in headers}
for r in rows:
for h in headers:
widths[h] = max(widths[h], len(str(r.get(h, ""))))
def line(char="-"):
print("+", end="")
for h in headers:
print(char * (widths[h] + 2) + "+", end="")
print()
# header
line("=")
print("| " + " | ".join(f"{h:{widths[h]}}" for h in headers) + " |")
line("=")
# rows
for r in rows:
vals = [r.get(h, "") for h in headers]
print("| " + " | ".join(f"{str(v):{widths[h]}}" for v, h in zip(vals, headers)) + " |")
line("=")
def main():
ap = argparse.ArgumentParser(
description="List all VS Code workspace IDs and their folders (URI kept as-is) from workspaceStorage (Stable/Insiders/OSS; Win/macOS/Linux)."
)
ap.add_argument("--csv", metavar="PATH", help="Write CSV to PATH instead of pretty table.")
ap.add_argument("--filter", help="Substring filter applied to URI (as-is).")
args = ap.parse_args()
rows = collect_all(args.filter)
if args.csv:
fieldnames = ["Flavor", "WorkspaceID", "URI", "StorePath"]
with open(args.csv, "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=fieldnames)
w.writeheader()
for r in rows:
w.writerow(r)
print(f"Wrote {len(rows)} rows to {args.csv}")
else:
print_table(rows)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import json
import os
import shutil
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse
def appdata_path(*parts: str) -> Path:
appdata = os.environ.get("APPDATA")
if not appdata:
print("ERROR: %APPDATA% not found. Please run on Windows.")
sys.exit(1)
return Path(appdata, *parts)
def load_json(p: Path) -> dict:
try:
with p.open("r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return {}
except json.JSONDecodeError as e:
print(f"ERROR: Failed to read JSON: {p}\n{e}")
sys.exit(1)
def save_json_atomic(p: Path, data: dict, dry_run: bool):
tmp = p.with_suffix(p.suffix + ".tmp")
if dry_run:
print(f"[DRY-RUN] Would write JSON to: {p}")
return
with tmp.open("w", encoding="utf-8", newline="\n") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
tmp.replace(p)
def backup_file(p: Path, dry_run: bool):
if not p.exists():
return
bak = p.with_suffix(p.suffix + ".bak")
if bak.exists():
i = 1
while True:
cand = p.with_suffix(p.suffix + f".bak.{i}")
if not cand.exists():
bak = cand
break
i += 1
if dry_run:
print(f"[DRY-RUN] Would create backup: {p} -> {bak}")
return
shutil.copy2(p, bak)
print(f"Backup: {p} -> {bak}")
def folder_uri_from_workspace_json(ws_json_path: Path) -> str | None:
j = load_json(ws_json_path)
candidates = [
j.get("folder"),
(j.get("configuration") or {}).get("folder"),
(j.get("workspace") or {}).get("folder"),
]
for c in candidates:
if isinstance(c, str) and c:
return c
return None
def remove_from_profile_associations(storage_json_path: Path, folder_path: Path, dry_run: bool) -> bool:
data = load_json(storage_json_path)
pa = (data.get("profileAssociations") or {})
workspaces = pa.get("workspaces")
if not isinstance(workspaces, dict):
return False
kept = {}
removed_any = False
for s, v in workspaces.items():
if s == folder_path:
removed_any = True
print(f"{'[DRY-RUN] Would remove from storage.json:' if dry_run else 'Removing from storage.json:'} {s}")
continue
kept[s] = v
if removed_any:
pa["workspaces"] = kept
data["profileAssociations"] = pa
backup_file(storage_json_path, dry_run)
save_json_atomic(storage_json_path, data, dry_run)
return removed_any
def delete_workspace_dir(ws_dir: Path, dry_run: bool):
if not ws_dir.exists():
print(f"INFO: Already does not exist: {ws_dir}")
return
if dry_run:
print(f"[DRY-RUN] Would delete directory: {ws_dir}")
return
print(f"Deleting directory: {ws_dir}")
shutil.rmtree(ws_dir)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Delete VS Code workspace info by workspace ID (with dry-run support)."
)
p.add_argument("--wsid", required=True, help="Folder name under workspaceStorage (workspace ID)")
p.add_argument("--dry-run", action="store_true", help="Show the plan only; do not modify or delete files")
return p.parse_args()
def main():
args = parse_args()
dry = args.dry_run
code_user = appdata_path("Code", "User")
ws_root = code_user / "workspaceStorage"
storage_json = code_user / "globalStorage" / "storage.json"
wsid = args.wsid.strip()
ws_dir = ws_root / wsid
ws_json = ws_dir / "workspace.json"
print(f"VS Code User dir : {code_user}")
print(f"workspaceStorage : {ws_root}")
print(f"storage.json : {storage_json}")
print(f"Target wsid : {wsid}")
print(f"workspace dir : {ws_dir}")
print(f"workspace.json : {ws_json}")
print(f"Mode : {'DRY-RUN' if dry else 'APPLY'}\n")
if not ws_json.exists():
print(f"ERROR: workspace.json not found: {ws_json}")
sys.exit(1)
# 1) workspace.json → folder URI
uri = folder_uri_from_workspace_json(ws_json)
if not uri:
print("WARNING: No folder URI found in workspace.json. The storage.json removal step may be skipped.")
folder_path = None
else:
folder_path = uri
print(f"Folder URI : {uri}")
print(f"Folder Path: {folder_path if folder_path else '(conversion failed)'}")
# 2) Remove the corresponding folder from `workspaces` in storage.json
if folder_path and storage_json.exists():
removed = remove_from_profile_associations(storage_json, folder_path, dry)
print("storage.json update:", "Removed target" if removed else "No target/No changes")
else:
if not storage_json.exists():
print("INFO: storage.json does not exist (skipping).")
# 3) Delete workspaceStorage/{wsid}
delete_workspace_dir(ws_dir, dry)
print("\nDone.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment