Created
May 26, 2026 00:46
-
-
Save bojanrajkovic/bf3cef8dbc6f360d03030eecfc9d79ea to your computer and use it in GitHub Desktop.
Copy Chrome localStorage entries from one origin to another (rewrites LevelDB directly)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // Copies Chrome localStorage entries from one origin to another. | |
| // Chrome must be closed before running. | |
| // | |
| // Usage: | |
| // node migrate.js <old-origin> <new-origin> [chrome-profile-path] | |
| // | |
| // Examples: | |
| // node migrate.js https://old.example.com https://new.example.com | |
| // node migrate.js https://old.example.com https://new.example.com "/path/to/Chrome/Default" | |
| 'use strict'; | |
| const { Level } = require('level'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const os = require('os'); | |
| function getDefaultChromePaths() { | |
| const home = os.homedir(); | |
| switch (process.platform) { | |
| case 'darwin': | |
| return [ | |
| path.join(home, 'Library/Application Support/Google/Chrome/Default'), | |
| path.join(home, 'Library/Application Support/Chromium/Default'), | |
| path.join(home, 'Library/Application Support/Google/Chrome Beta/Default'), | |
| ]; | |
| case 'win32': | |
| return [ | |
| path.join(process.env.LOCALAPPDATA || '', 'Google/Chrome/User Data/Default'), | |
| path.join(process.env.LOCALAPPDATA || '', 'Chromium/User Data/Default'), | |
| path.join(process.env.LOCALAPPDATA || '', 'Google/Chrome Beta/User Data/Default'), | |
| ]; | |
| default: // Linux and other POSIX | |
| return [ | |
| path.join(home, '.config/google-chrome/Default'), | |
| path.join(home, '.config/chromium/Default'), | |
| path.join(home, '.config/google-chrome-beta/Default'), | |
| path.join(home, 'snap/chromium/common/chromium/Default'), | |
| ]; | |
| } | |
| } | |
| function findDbPath(profilePath) { | |
| const candidates = profilePath | |
| ? [profilePath] | |
| : getDefaultChromePaths(); | |
| for (const p of candidates) { | |
| const dbPath = path.join(p, 'Local Storage/leveldb'); | |
| if (fs.existsSync(dbPath)) return dbPath; | |
| } | |
| return null; | |
| } | |
| async function migrate(oldOrigin, newOrigin, dbPath) { | |
| console.log(`DB: ${dbPath}`); | |
| console.log(`From: ${oldOrigin}`); | |
| console.log(`To: ${newOrigin}\n`); | |
| // Warn if LOCK file suggests Chrome might be open | |
| const lockFile = path.join(dbPath, 'LOCK'); | |
| if (fs.existsSync(lockFile) && fs.statSync(lockFile).size > 0) { | |
| console.warn('WARNING: LOCK file is non-empty — Chrome may still be running. Proceed at your own risk.\n'); | |
| } | |
| const db = new Level(dbPath, { keyEncoding: 'buffer', valueEncoding: 'buffer' }); | |
| await db.open(); | |
| const oldDataPrefix = Buffer.from(`_${oldOrigin}\x00\x01`); | |
| const oldMetaKey = Buffer.from(`META:${oldOrigin}`); | |
| const oldAccessKey = Buffer.from(`METAACCESS:${oldOrigin}`); | |
| const toWrite = []; | |
| const found = []; | |
| for await (const [key, value] of db.iterator()) { | |
| const keyBin = key.toString('binary'); | |
| if (key.equals(oldMetaKey)) { | |
| toWrite.push({ type: 'put', key: Buffer.from(`META:${newOrigin}`), value }); | |
| found.push(`META:${newOrigin}`); | |
| } else if (key.equals(oldAccessKey)) { | |
| toWrite.push({ type: 'put', key: Buffer.from(`METAACCESS:${newOrigin}`), value }); | |
| found.push(`METAACCESS:${newOrigin}`); | |
| } else if (keyBin.startsWith(oldDataPrefix.toString('binary'))) { | |
| const suffix = keyBin.slice(oldDataPrefix.toString('binary').length); | |
| toWrite.push({ type: 'put', key: Buffer.from(`_${newOrigin}\x00\x01${suffix}`), value }); | |
| found.push(` ${suffix}`); | |
| } | |
| } | |
| if (toWrite.length === 0) { | |
| console.log(`No localStorage entries found for ${oldOrigin}`); | |
| await db.close(); | |
| return; | |
| } | |
| console.log(`Found ${toWrite.length} entries:`); | |
| for (const k of found) console.log(` ${k}`); | |
| await db.batch(toWrite); | |
| console.log(`\nWrote ${toWrite.length} entries to ${newOrigin}`); | |
| await db.close(); | |
| } | |
| // ── main ────────────────────────────────────────────────────────────────────── | |
| const [,, oldOrigin, newOrigin, profileArg] = process.argv; | |
| if (!oldOrigin || !newOrigin) { | |
| console.error('Usage: node migrate.js <old-origin> <new-origin> [chrome-profile-path]'); | |
| console.error('Example: node migrate.js https://old.example.com https://new.example.com'); | |
| process.exit(1); | |
| } | |
| const dbPath = findDbPath(profileArg); | |
| if (!dbPath) { | |
| console.error('Could not find Chrome localStorage directory. Pass it explicitly as the third argument.'); | |
| process.exit(1); | |
| } | |
| migrate(oldOrigin, newOrigin, dbPath).catch(err => { | |
| console.error(err.message || err); | |
| process.exit(1); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "chrome-localstorage-origin-migrate", | |
| "version": "1.0.0", | |
| "description": "Copy Chrome localStorage entries from one origin to another by rewriting the underlying LevelDB", | |
| "main": "migrate.js", | |
| "bin": { | |
| "chrome-ls-migrate": "migrate.js" | |
| }, | |
| "scripts": { | |
| "start": "node migrate.js" | |
| }, | |
| "keywords": ["chrome", "localstorage", "leveldb", "migrate"], | |
| "license": "MIT", | |
| "type": "commonjs", | |
| "dependencies": { | |
| "level": "^10.0.0" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment