Skip to content

Instantly share code, notes, and snippets.

@XueshiQiao
Last active April 11, 2026 03:09
Show Gist options
  • Select an option

  • Save XueshiQiao/8319f4e4afb61058768e86353e49b4c9 to your computer and use it in GitHub Desktop.

Select an option

Save XueshiQiao/8319f4e4afb61058768e86353e49b4c9 to your computer and use it in GitHub Desktop.
Bulk list & delete all saved tab groups from Chrome on macOS (no extension API available — works by reading/writing Chrome's internal LevelDB)
#!/usr/bin/env node
/**
* Chrome Saved Tab Groups Manager
* ================================
* Bulk list and delete all saved tab groups from a Chrome profile on macOS.
*
* Chrome does NOT expose saved tab groups to extensions or DevTools — there is
* no public API for this. This script works by directly reading/writing the
* LevelDB that Chrome Sync uses internally.
*
*
* How it works
* ------------
* Chrome stores saved tab groups in two places:
*
* 1. Sync Data LevelDB
* ~/Library/Application Support/Google/Chrome/<Profile>/Sync Data/LevelDB
* Keys prefixed with "saved_tab_group-" hold the actual data:
* - saved_tab_group-dt-<uuid> → group definitions (title, color) and
* individual tab entries (URL, page title)
* - saved_tab_group-md-<uuid> → sync metadata per entry
* - saved_tab_group-GlobalMetadata → global sync state
* Values are protobuf-encoded. This script extracts readable strings
* (titles, URLs) via regex without a full protobuf decode.
*
* 2. Preferences JSON
* ~/Library/Application Support/Google/Chrome/<Profile>/Preferences
* The "saved_tab_groups" key holds auxiliary state such as
* closed_remote_group_ids and deleted_group_ids.
*
* "list" mode copies the LevelDB to a temp directory (to avoid lock conflicts
* with a running Chrome) and reads from the copy.
*
* "delete" mode requires Chrome to be fully closed. It deletes all
* saved_tab_group-* keys from the LevelDB in place, and removes the
* saved_tab_groups key from Preferences. A timestamped backup of both is
* created automatically before any modification.
*
*
* Chrome Sync caveat
* ------------------
* This script only deletes LOCAL data. If Chrome Sync is enabled for saved tab
* groups, Google's sync server still retains a copy. When you re-enable sync
* (or it was never disabled), Chrome will re-download everything from the
* server and the tab groups will reappear.
*
* To handle this, you have two options:
*
* Option A — Keep "Saved Tab Groups" sync turned off (recommended)
* 1. chrome://settings/syncSetup → disable "Saved Tab Groups"
* 2. Close Chrome (Cmd+Q)
* 3. Run: node chrome-clear-saved-tab-groups.mjs delete
* 4. Open Chrome — done. Tab groups stay gone as long as sync is off.
*
* Option B — Nuke Google sync server data, then delete locally
* 1. Open Chrome, go to chrome.google.com/sync
* 2. Scroll to the bottom → click "Clear Data"
* (This wipes ALL Chrome sync data from Google's servers. Local data
* on this machine — bookmarks, passwords, etc. — is untouched and
* will be re-uploaded as the new baseline when sync restarts.)
* 3. Close Chrome (Cmd+Q)
* 4. Run: node chrome-clear-saved-tab-groups.mjs delete
* 5. Open Chrome — sync will re-upload your now-clean local state.
*
*
* Usage
* -----
* node chrome-clear-saved-tab-groups.mjs list List all saved tab groups
* node chrome-clear-saved-tab-groups.mjs list -v List with full details
* node chrome-clear-saved-tab-groups.mjs delete Delete all saved tab groups
* node chrome-clear-saved-tab-groups.mjs list --profile "Profile 1"
*
* Options:
* --profile <name> Chrome profile (default: "Default")
* Examples: "Default", "Profile 1", "Profile 3"
* -v, --verbose Show UUIDs, individual tabs, and URLs
*
* Prerequisites
* -------------
* npm install classic-level (run once in the same directory)
*
* Platform: macOS only (Chrome profile path is hardcoded to ~/Library/...).
*/
import { ClassicLevel } from "classic-level";
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { execSync } from "child_process";
import { cpSync, mkdirSync, rmSync } from "fs";
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const CHROME_BASE = join(
homedir(),
"Library",
"Application Support",
"Google",
"Chrome"
);
function profileDir(profileName) {
return join(CHROME_BASE, profileName);
}
function syncLevelDBPath(profileName) {
return join(profileDir(profileName), "Sync Data", "LevelDB");
}
function preferencesPath(profileName) {
return join(profileDir(profileName), "Preferences");
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function isChromeRunning() {
try {
const out = execSync("pgrep -x 'Google Chrome'", { encoding: "utf8" });
return out.trim().length > 0;
} catch {
return false;
}
}
/** Extract readable ASCII strings from a buffer */
function extractStrings(buf, minLen = 4) {
const text = buf.toString("utf8", 0, Math.min(buf.length, 5000));
return (text.match(/[\x20-\x7e]{3,}/g) || []).filter(
(s) =>
!s.match(/^[a-f0-9\-]{20,}$/) &&
!s.match(/^[\+\/=A-Za-z0-9]{20,}$/) &&
!s.startsWith("$")
);
}
/** Extract URLs from a buffer */
function extractUrls(buf) {
const text = buf.toString("utf8", 0, Math.min(buf.length, 5000));
return (text.match(/https?:\/\/[^\x00-\x1f"'\s\\)]{5,}/g) || []);
}
// ---------------------------------------------------------------------------
// List
// ---------------------------------------------------------------------------
async function listSavedTabGroups(profileName) {
const dbPath = syncLevelDBPath(profileName);
if (!existsSync(dbPath)) {
console.error(`LevelDB not found at: ${dbPath}`);
process.exit(1);
}
// Copy the DB so we don't need Chrome to be closed
const tmpDir = join("/tmp", `chrome-stg-list-${Date.now()}`);
mkdirSync(tmpDir, { recursive: true });
cpSync(dbPath, tmpDir, { recursive: true });
// Remove lock from copy
const lockFile = join(tmpDir, "LOCK");
if (existsSync(lockFile)) rmSync(lockFile);
const db = new ClassicLevel(tmpDir, {
createIfMissing: false,
keyEncoding: "buffer",
valueEncoding: "buffer",
});
await db.open();
// Collect all saved_tab_group-dt entries
const dtEntries = new Map();
for await (const [key, value] of db.iterator()) {
const keyStr = key.toString("latin1");
const m = keyStr.match(/saved_tab_group-dt-([0-9a-f\-]{36})/);
if (m) {
dtEntries.set(m[1], value);
}
}
await db.close();
// Clean up temp copy
rmSync(tmpDir, { recursive: true, force: true });
// Classify into groups vs tabs
const groups = []; // entries with no URLs (group definitions)
const tabs = []; // entries with URLs (tab definitions)
for (const [uuid, value] of dtEntries) {
const urls = extractUrls(value);
const strings = extractStrings(value);
if (urls.length > 0) {
// This is a tab entry
// Try to get the page title from strings
const titleCandidates = strings.filter((s) => !s.startsWith("http"));
const pageTitle = titleCandidates.find(s => s.length > 2 && !s.match(/^[\W]{1,3}$/)) || "(untitled)";
tabs.push({ uuid, urls, title: pageTitle });
} else {
// This is a group entry
const nonUuidStrings = strings.filter(
(s) => !s.match(/^[0-9a-f]{8}-[0-9a-f]{4}/)
);
const raw = nonUuidStrings[0] || "(untitled)";
// Clean leading # or digits that are protobuf artifacts, and trailing quote
const groupName = raw.replace(/^[#\d]+/, "").replace(/["']$/, "").trim() || raw;
groups.push({ uuid, title: groupName });
}
}
// Display results
console.log(`\nProfile: ${profileName}`);
console.log(`${"=".repeat(60)}`);
console.log(`Saved Tab Groups: ${groups.length}`);
console.log(`Total Tabs: ${tabs.length}`);
console.log(`Total LevelDB entries (dt + md + global): ${dtEntries.size} dt entries`);
console.log(`${"=".repeat(60)}\n`);
// Deduplicate group names and count
const nameCount = {};
for (const g of groups) {
nameCount[g.title] = (nameCount[g.title] || 0) + 1;
}
const sortedNames = Object.entries(nameCount).sort((a, b) => b[1] - a[1]);
console.log("Groups by name:");
for (const [name, count] of sortedNames) {
const suffix = count > 1 ? ` (x${count})` : "";
console.log(` - ${name}${suffix}`);
}
console.log();
// Show detailed list
if (process.argv.includes("--verbose") || process.argv.includes("-v")) {
console.log("Detailed group list:");
for (const g of groups) {
console.log(` [${g.uuid}] ${g.title}`);
}
console.log("\nTab list:");
for (const t of tabs) {
console.log(` [${t.uuid}] ${t.title}`);
for (const u of t.urls) {
console.log(` ${u}`);
}
}
}
}
// ---------------------------------------------------------------------------
// Delete
// ---------------------------------------------------------------------------
async function deleteSavedTabGroups(profileName) {
// Safety check: Chrome must be closed
if (isChromeRunning()) {
console.error(
"\n*** ERROR: Chrome is still running! ***\n" +
"Please fully quit Chrome (Cmd+Q) before running delete mode.\n" +
"Just closing the window is not enough.\n"
);
process.exit(1);
}
const dbPath = syncLevelDBPath(profileName);
const prefsPath = preferencesPath(profileName);
if (!existsSync(dbPath)) {
console.error(`LevelDB not found at: ${dbPath}`);
process.exit(1);
}
// ---- Step 1: Backup ----
const backupSuffix = new Date().toISOString().replace(/[:.]/g, "-");
const dbBackup = `${dbPath}.backup-${backupSuffix}`;
const prefsBackup = `${prefsPath}.backup-${backupSuffix}`;
console.log("\n--- Step 1: Creating backups ---");
cpSync(dbPath, dbBackup, { recursive: true });
console.log(` LevelDB backed up to: ${dbBackup}`);
if (existsSync(prefsPath)) {
copyFileSync(prefsPath, prefsBackup);
console.log(` Preferences backed up to: ${prefsBackup}`);
}
// ---- Step 2: Delete from LevelDB ----
console.log("\n--- Step 2: Deleting saved tab group entries from LevelDB ---");
const db = new ClassicLevel(dbPath, {
createIfMissing: false,
keyEncoding: "buffer",
valueEncoding: "buffer",
});
await db.open();
let deleteCount = 0;
const keysToDelete = [];
for await (const [key] of db.iterator()) {
const keyStr = key.toString("latin1");
if (keyStr.includes("saved_tab_group")) {
keysToDelete.push(key);
}
}
const batch = db.batch();
for (const key of keysToDelete) {
batch.del(key);
deleteCount++;
}
await batch.write();
await db.close();
console.log(` Deleted ${deleteCount} LevelDB entries`);
// ---- Step 3: Clean up Preferences JSON ----
console.log("\n--- Step 3: Cleaning up Preferences ---");
if (existsSync(prefsPath)) {
const prefs = JSON.parse(readFileSync(prefsPath, "utf8"));
let changed = false;
if (prefs.saved_tab_groups) {
delete prefs.saved_tab_groups;
changed = true;
console.log(" Removed 'saved_tab_groups' from Preferences");
}
if (prefs.tab_group_saves_ui_update_migrated !== undefined) {
delete prefs.tab_group_saves_ui_update_migrated;
changed = true;
}
if (changed) {
writeFileSync(prefsPath, JSON.stringify(prefs, null, 3), "utf8");
console.log(" Preferences file updated");
} else {
console.log(" No saved_tab_groups found in Preferences (already clean)");
}
}
// ---- Done ----
console.log("\n--- Done! ---");
console.log("You can now open Chrome. All saved tab groups should be gone.");
console.log(
"\nNOTE: If Chrome Sync is enabled for saved tab groups, they may re-appear."
);
console.log("See the comment block at the top of this script for how to handle sync.");
console.log(`\nIf something went wrong, restore from backup:`);
console.log(` rm -rf "${dbPath}"`);
console.log(` mv "${dbBackup}" "${dbPath}"`);
console.log(` cp "${prefsBackup}" "${prefsPath}"`);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
const command = args.find((a) => !a.startsWith("-"));
const profileIdx = args.indexOf("--profile");
const profileName =
profileIdx >= 0 && args[profileIdx + 1] ? args[profileIdx + 1] : "Default";
if (!command || !["list", "delete"].includes(command)) {
console.log(`
Chrome Saved Tab Groups Manager
Usage:
node chrome-clear-saved-tab-groups.mjs list List all saved tab groups
node chrome-clear-saved-tab-groups.mjs list -v List with full details (UUIDs, URLs)
node chrome-clear-saved-tab-groups.mjs delete Delete all saved tab groups
Options:
--profile <name> Chrome profile to use (default: "Default")
Examples: "Default", "Profile 1", "Profile 3"
-v, --verbose Show detailed output (UUIDs, individual tabs, URLs)
Notes:
- "list" works while Chrome is running (reads a snapshot)
- "delete" requires Chrome to be fully closed (Cmd+Q)
- Backups are created automatically before deletion
- See the comment block at the top of this file for Chrome Sync handling
`);
process.exit(0);
}
if (command === "list") {
await listSavedTabGroups(profileName);
} else if (command === "delete") {
await deleteSavedTabGroups(profileName);
}
@XueshiQiao
Copy link
Copy Markdown
Author

XueshiQiao commented Apr 11, 2026

Quick start

# 1. Download
curl -sLO https://gist.githubusercontent.com/XueshiQiao/8319f4e4afb61058768e86353e49b4c9/raw/chrome-clear-saved-tab-groups.mjs && npm install classic-level

# 2. Run
node chrome-clear-saved-tab-groups.mjs list      # list all saved tab groups
node chrome-clear-saved-tab-groups.mjs delete     # delete all (quit Chrome first)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment