Skip to content

Instantly share code, notes, and snippets.

@erbanku
Last active March 11, 2026 14:49
Show Gist options
  • Select an option

  • Save erbanku/eff0c8e593b54d4dd3ac7ffaa1325dfc to your computer and use it in GitHub Desktop.

Select an option

Save erbanku/eff0c8e593b54d4dd3ac7ffaa1325dfc to your computer and use it in GitHub Desktop.
Chrome saved tab groups cleanup tools and usage guide

Saved Tab Groups Cleanup Tool

This repository includes a local cleanup tool for removing Chrome saved tab groups directly from the profile sync database.

Use it only when Chrome is fully closed.

What It Does

The tool deletes LevelDB keys with the prefix:

saved_tab_group-dt-*

For the selected Chrome profile, this removes saved tab groups that are not exposed through the normal Chrome Extensions API.

Important Safety Rules

  1. Fully close Chrome before running the tool.
  2. Back up the full Chrome profile before any cleanup.
  3. Run the tool in dry-run mode first.
  4. Review the generated report before using -apply.
  5. If Chrome Sync has Open Tabs enabled, Chrome may restore saved groups after cleanup.

Backup First

Example full backup of Chrome user data:

$src = Join-Path $env:LOCALAPPDATA 'Google\Chrome\User Data'
$dest = 'E:\Backups\Chrome\User Data'
robocopy $src $dest /MIR /R:1 /W:1 /XJ /NFL /NDL /NP /MT:16

Prerequisites

  • Go installed
  • Chrome fully closed
  • Access to the local Chrome profile directory

Install Go dependencies once:

go mod tidy

Inspect Saved Tab Groups

To count matching saved tab group keys for a profile:

go run ./tools/inspect_saved_tab_groups.go -profile Default

Example for another profile:

go run ./tools/inspect_saved_tab_groups.go -profile "Profile 10"

Dry Run

Dry-run mode only reports what would be deleted.

go run ./tools/clean_saved_tab_groups.go -profile Default

This writes a report under:

reports\saved-tab-groups\

Apply Deletion

After reviewing the dry-run report:

go run ./tools/clean_saved_tab_groups.go -profile Default -apply

This deletes matching saved_tab_group-dt-* keys and prints the remaining count.

Report Files

The tool writes a timestamped report for both dry-run and apply mode.

Examples:

  • reports\saved-tab-groups\Default-dry-run-20260311-224334.txt
  • reports\saved-tab-groups\Default-apply-20260311-224344.txt

Each report includes:

  • profile name
  • database path
  • whether -apply was used
  • number of matched keys
  • full list of matched keys

Rollback

If needed, restore the full backup after closing Chrome:

$src = 'E:\Backups\Chrome\User Data'
$dest = Join-Path $env:LOCALAPPDATA 'Google\Chrome\User Data'
robocopy $src $dest /MIR /R:1 /W:1 /XJ /NFL /NDL /NP /MT:16

Sync Warning

If Chrome restores the groups after cleanup, check Chrome Sync settings.

The most likely cause is sync for Open Tabs.

Recommended:

  1. Open Chrome Sync settings.
  2. Turn off Open Tabs.
  3. Close Chrome again.
  4. Run the cleanup tool.
  5. Reopen Chrome and confirm the groups are gone.

Files

  • tools/inspect_saved_tab_groups.go
  • tools/clean_saved_tab_groups.go
  • reports/saved-tab-groups/

Scope

This tool currently targets only:

  • saved_tab_group-dt-*

It does not delete unrelated sync records, bookmarks, history, tabs, passwords, or other Chrome profile data.

param(
[string]$Destination = 'E:\Backups\Chrome\User Data'
)
$source = Join-Path $env:LOCALAPPDATA 'Google\Chrome\User Data'
if (-not (Test-Path $source)) {
throw "Chrome User Data not found at: $source"
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
robocopy $source $Destination /MIR /R:1 /W:1 /XJ /NFL /NDL /NP /MT:16
$exitCode = $LASTEXITCODE
if ($exitCode -ge 8) {
throw "Backup failed with robocopy exit code $exitCode"
}
Write-Output "Backup complete: $Destination"
Write-Output "robocopy exit code: $exitCode"
package main
import (
"bufio"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"time"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
func main() {
profileName := flag.String("profile", "Default", "Chrome profile name")
apply := flag.Bool("apply", false, "Delete matching keys instead of running a dry run")
reportDir := flag.String("report-dir", filepath.Join("reports", "saved-tab-groups"), "Directory for key reports")
flag.Parse()
dbPath := filepath.Join(
os.Getenv("LOCALAPPDATA"),
"Google",
"Chrome",
"User Data",
*profileName,
"Sync Data",
"LevelDB",
)
db, err := leveldb.OpenFile(dbPath, nil)
if err != nil {
log.Fatalf("open leveldb: %v", err)
}
defer db.Close()
const prefix = "saved_tab_group-dt-"
iter := db.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
keys := make([]string, 0, 1024)
for iter.Next() {
keys = append(keys, string(iter.Key()))
}
if err := iter.Error(); err != nil {
iter.Release()
log.Fatalf("iterate keys: %v", err)
}
iter.Release()
sort.Strings(keys)
if err := os.MkdirAll(*reportDir, 0o755); err != nil {
log.Fatalf("create report dir: %v", err)
}
reportPath := filepath.Join(
*reportDir,
fmt.Sprintf(
"%s-%s-%s.txt",
*profileName,
map[bool]string{true: "apply", false: "dry-run"}[*apply],
time.Now().Format("20060102-150405"),
),
)
reportFile, err := os.Create(reportPath)
if err != nil {
log.Fatalf("create report file: %v", err)
}
defer reportFile.Close()
writer := bufio.NewWriter(reportFile)
fmt.Fprintf(writer, "profile=%s\n", *profileName)
fmt.Fprintf(writer, "db=%s\n", dbPath)
fmt.Fprintf(writer, "apply=%t\n", *apply)
fmt.Fprintf(writer, "count=%d\n", len(keys))
for _, key := range keys {
fmt.Fprintln(writer, key)
}
if err := writer.Flush(); err != nil {
log.Fatalf("flush report: %v", err)
}
fmt.Printf("Profile: %s\n", *profileName)
fmt.Printf("DB: %s\n", dbPath)
fmt.Printf("Matched keys: %d\n", len(keys))
fmt.Printf("Report: %s\n", reportPath)
if !*apply {
fmt.Println("Mode: dry-run")
return
}
batch := new(leveldb.Batch)
for _, key := range keys {
batch.Delete([]byte(key))
}
if err := db.Write(batch, nil); err != nil {
log.Fatalf("delete keys: %v", err)
}
verifyIter := db.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
remaining := 0
for verifyIter.Next() {
remaining++
}
if err := verifyIter.Error(); err != nil {
verifyIter.Release()
log.Fatalf("verify keys: %v", err)
}
verifyIter.Release()
fmt.Printf("Deleted keys: %d\n", len(keys))
fmt.Printf("Remaining keys: %d\n", remaining)
}
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
func main() {
profileName := flag.String("profile", "Default", "Chrome profile name")
flag.Parse()
dbPath := filepath.Join(
os.Getenv("LOCALAPPDATA"),
"Google",
"Chrome",
"User Data",
*profileName,
"Sync Data",
"LevelDB",
)
db, err := leveldb.OpenFile(dbPath, nil)
if err != nil {
log.Fatalf("open leveldb: %v", err)
}
defer db.Close()
prefixes := []string{
"saved_tab_group",
"saved_tab_group-",
}
seen := map[string]int{}
for _, prefix := range prefixes {
iter := db.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
for iter.Next() {
key := string(iter.Key())
seen[key]++
}
if err := iter.Error(); err != nil {
iter.Release()
log.Fatalf("iterate prefix %q: %v", prefix, err)
}
iter.Release()
}
keys := make([]string, 0, len(seen))
for key := range seen {
keys = append(keys, key)
}
sort.Strings(keys)
fmt.Printf("Profile: %s\n", *profileName)
fmt.Printf("DB: %s\n", dbPath)
fmt.Printf("Matched keys: %d\n", len(keys))
grouped := map[string]int{}
for _, key := range keys {
label := key
if strings.HasPrefix(key, "saved_tab_group-dt-") {
label = "saved_tab_group-dt-*"
} else if strings.HasPrefix(key, "saved_tab_group-md-") {
label = "saved_tab_group-md-*"
}
grouped[label]++
}
labels := make([]string, 0, len(grouped))
for label := range grouped {
labels = append(labels, label)
}
sort.Strings(labels)
for _, label := range labels {
fmt.Printf("%s => %d\n", label, grouped[label])
}
limit := 30
if len(keys) < limit {
limit = len(keys)
}
fmt.Println("Sample keys:")
for _, key := range keys[:limit] {
fmt.Println(key)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment