merge-firefox-history
Merge the browsing history of two Firefox profiles by combining their
places.sqlite
databases. The target profile keeps its own history and gains the history from
the other profile; the source database is never modified. Current Firefox
releases and older profiles are supported.
Requirements
- Bash 4.0 or newer (macOS ships Bash 3.2; install a newer one via Homebrew)
- SQLite 3.24.0 or newer (
sqlite3inPATH) - Optional:
python3, to fill in URL hashes imported from very old profiles - Recommended:
fuserorlsof, so the script can detect a running Firefox
Usage
./merge-firefox-history.sh <db1> <db2> [backup_location] [options]
<db1> is the target database and receives the history from <db2>. <db2>
is never modified.
Close Firefox before using a database as the target. A timestamped backup of
<db1> is created before anything is changed.
Example
./merge-firefox-history.sh \
~/.mozilla/firefox/profile1.default-release/places.sqlite \
~/.mozilla/firefox/profile2.default-release/places.sqlite
This adds the history from profile2 to profile1 and writes a timestamped
backup of profile1/places.sqlite to the current directory.
Options
| Option | Description |
|---|---|
-b, --backup-dir DIR |
Directory for the backup of <db1> (default: current directory). |
-o, --output FILE |
Write the result to FILE instead of modifying <db1>. <db1> is only read. |
-n, --dry-run |
Do the merge on a temporary copy and discard it. |
-s, --skip-vacuum |
Do not VACUUM the database (faster on large profiles). |
-y, --yes |
Do not ask for confirmation. |
-f, --force |
Overwrite an existing --output file. |
--keep-temp |
Keep the temporary working directory (for debugging). |
-h, --help |
Show help. |
The third positional argument is a backwards-compatible alias for
--backup-dir.
Merge into a new file
To leave both originals untouched, write the result to a separate file:
./merge-firefox-history.sh \
~/.mozilla/firefox/profile1.default-release/places.sqlite \
~/.mozilla/firefox/profile2.default-release/places.sqlite \
--output ~/merged-places.sqlite
Then replace the profile’s places.sqlite with the merged file while Firefox
is closed, keeping a copy of the original.
Preview the result
Use --dry-run to perform the merge on a temporary copy and report how many
places and visits would be added.
Where is places.sqlite?
In Firefox, open about:support and use Profile Directory → Open Directory.
The database is the file named places.sqlite in that directory.
Firefox stores profiles in:
- Linux:
~/.mozilla/firefox/ - macOS:
~/Library/Application Support/Firefox/Profiles/ - Windows:
%APPDATA%\Mozilla\Firefox\Profiles\
Safety
Both inputs are snapshotted with SQLite’s online backup API before use, so the snapshot includes committed WAL contents. The source is opened read-only and is never modified. There is no unsafe file-copy fallback: if a snapshot cannot be taken, the run fails.
Before anything is changed, the target database is written to a timestamped
backup. If the merge, a VACUUM, or the final verification fails, the backup is
restored automatically. The generated merge statements share one transaction;
the optional URL-hash update and VACUUM run afterwards and are covered by the
same restore-on-failure path.
Filling url_hash is best-effort. If python3 is unavailable, or hashing
fails, the merge still succeeds with a warning and Firefox calculates the
missing hashes during idle maintenance. Use --skip-vacuum when compaction is
not wanted or cannot be completed.
A database that another process, usually Firefox, has open is never modified in
place. --output and --dry-run work from a consistent snapshot instead. The
script also refuses --output paths that refer to either input database, even
through a hard link. An output file is written only after the merged database
has passed PRAGMA quick_check and the foreign-key check; a failed run leaves
an existing output file untouched.
PRAGMA foreign_key_check is compared with the target’s pre-merge baseline.
Only violations introduced by the merge are fatal; pre-existing ones are
reported but do not block the merge. Foreign-key checks cannot validate Places
relationships that a particular schema does not declare as constraints.
Keep in mind that only one profile can use a given places.sqlite at a time, so
close Firefox before swapping files.
Output
Progress is written to stderr and appended to merge.log in the current
directory. Set the MERGE_LOG environment variable to use another file.
[2026-10-20 23:04:03] merge-firefox-history: target='.../profile1/places.sqlite' source='.../profile2/places.sqlite'
[2026-10-20 23:04:03] Backup created: ./places.sqlite.backup_20261020_230403
[2026-10-20 23:04:07] Merge completed successfully.
[2026-10-20 23:04:07] Places added: 101628 (now 203148)
[2026-10-20 23:04:07] Visits added: 215507 (now 442654; skipped as duplicate or unmapped: 0)
Troubleshooting
- “database is open in another process” – close Firefox (or the other
program using the file) before an in-place merge. Use
--output/--dry-runto work from a consistent snapshot without closing anything. - “database is locked” – another process still has a write lock. Close Firefox and try again.
- Integrity check failed – the database is damaged. Do not merge it. Make
a copy and inspect it with
sqlite3 places.sqlite "PRAGMA integrity_check;". Firefox can sometimes rebuild a damaged database by renamingplaces.sqlitewhile it is closed. - Frecency looks odd right after merging – Firefox recalculates it on idle; open Firefox and leave it running for a moment.
- Large profiles – merging several hundred thousand visits takes a few
seconds, but
VACUUMcan take longer. Use--skip-vacuumif needed.
What is not merged
Favicons live in favicons.sqlite, a separate database, and bookmarks,
keywords, annotations, passwords, tabs, add-ons and preferences are all outside
the scope of a history merge. Sync metadata is also left alone, because merging
it between two profiles could cause duplicated uploads or misleading
statistics.
History is matched by URL, so metadata that exists in both profiles (for
example a title changed in one profile) is not reconciled. Pages imported from
profiles older than the moz_origins schema may have no origin assigned;
Firefox still displays them, only origin-based frecency is affected.
This is not a Sync replacement. See How it works for the merged tables, deduplication rules, and schema compatibility details.
License
This script is released under the ISC License.