Skip to content

Instantly share code, notes, and snippets.

@bluwy
Last active January 9, 2026 05:58
Show Gist options
  • Select an option

  • Save bluwy/90d3fe350158d83f315c2bb50921616f to your computer and use it in GitHub Desktop.

Select an option

Save bluwy/90d3fe350158d83f315c2bb50921616f to your computer and use it in GitHub Desktop.
Script to clear Safari history older than N days (and deletes other unnecessary data)
import fs from 'node:fs'
import path from 'node:path'
import { DatabaseSync } from 'node:sqlite'
/*
How to run?
1. Copy from ~/Library/Safari/History.db to current directory
2. Run: node clear-safari-history.js (Node.js v24 required)
3. Paste the updated History.db back to ~/Library/Safari/History.db (backup the original file if needed)
Adjust the const variables below as needed.
*/
const historyDbPath = path.resolve('./History.db')
const deleteItemsOlderThanDays = 90
const deleteTombstone = true
const deleteAllTags = false
if (!fs.existsSync(historyDbPath)) {
console.error('Safari history database not found.')
process.exit(1)
}
/*
Notable Database Structure:
- `history_visits`: The history record.
- `visit_time`: Timestamp of the visit (in seconds since 2001-01-01)
- `history_items`: Metadata of each history url entry
- `history_items_to_tags`: Tags associated with history items
- `history_tags`: The available tags
*/
/*
Steps:
1. Query and delete `history_visits` for entries older than the cutoff date.
2. For each deleted visits' `history_item`, use as foreign key to query and delete from `history_items` `id`.
3. For each deleted history items' `id`, use as foreign key to query and delete from `history_items_to_tags` `history_item`.
4. For each deleted tag associations' `tag_id`, use as foreign key to query from `history_tags` `id`, and reduce the `item_count` by 1.
*/
const db = new DatabaseSync(historyDbPath)
// debugDatabase(db)
// process.exit(0)
const currentMacEpochTime = getCurrentMacEpochTime()
const cutoffMacEpochTime = currentMacEpochTime - deleteItemsOlderThanDays * 24 * 60 * 60
const result = db
.prepare('DELETE FROM history_visits WHERE visit_time < ? RETURNING history_item')
.all(cutoffMacEpochTime)
console.log(
`Deleted ${result.length} history_visits entries older than ${deleteItemsOlderThanDays} days.`
)
const deletedHistoryIds = unique(result.map((row) => row.history_item))
if (deletedHistoryIds.length > 0) {
const result = db
.prepare(
`DELETE FROM history_items WHERE id IN (${deletedHistoryIds.map(() => '?').join(',')})`
)
.run(...deletedHistoryIds)
console.log(`Deleted ${result.changes} history_items entries.`)
}
if (deleteAllTags) {
const result = db.prepare('DELETE FROM history_tags').run()
console.log(`Deleted ${result.changes} history_tags entries.`)
const result2 = db.prepare('DELETE FROM history_items_to_tags').run()
console.log(`Deleted ${result2.changes} history_items_to_tags entries.`)
} else if (deletedHistoryIds.length > 0) {
const result2 = db
.prepare(
`DELETE FROM history_items_to_tags WHERE history_item IN (${deletedHistoryIds
.map(() => '?')
.join(',')}) RETURNING tag_id`
)
.all(...deletedHistoryIds)
console.log(`Deleted ${result2.length} history_items_to_tags entries.`)
const tagIdToNum = {}
for (const row of result2) {
if (!(row.tag_id in tagIdToNum)) {
tagIdToNum[row.tag_id] = 0
}
tagIdToNum[row.tag_id] += 1
}
for (const [tagId, num] of Object.entries(tagIdToNum)) {
db.prepare('UPDATE history_tags SET item_count = MAX(item_count - ?, 0) WHERE id = ?').run(
num,
tagId
)
console.log(`Updated history_tags id=${tagId}, decreased item_count by ${num}.`)
}
// Empty all soft delete data
if (deleteTombstone) {
const result = db.prepare('DELETE FROM history_tombstones').run()
console.log(`Deleted ${result.changes} history_tombstones entries.`)
}
// Vacuum the database to reclaim space
db.prepare('VACUUM').run()
console.log('Database vacuumed to reclaim space.')
console.log('Safari history cleanup completed.')
}
/**
* @param {number} time
*/
function getDateFromMacEpochTime(time) {
const macEpoch = new Date('2001-01-01T00:00:00Z')
const date = new Date(macEpoch.getTime() + time * 1000)
return date
}
function getCurrentMacEpochTime() {
const macEpoch = new Date('2001-01-01T00:00:00Z')
const now = new Date()
const secondsSinceMacEpoch = Math.floor((now.getTime() - macEpoch.getTime()) / 1000)
return secondsSinceMacEpoch
}
/**
* @param {DatabaseSync} db
*/
function debugDatabase(db) {
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all()
// Log the first 10 rows of each table
for (const table of tables) {
const tableName = table.name
const rows = db.prepare(`SELECT * FROM ${tableName} LIMIT 10`).all()
console.log(`Table: ${tableName}`)
console.table(rows)
}
}
/**
* @param {T[]} array
* @template T
* @return {T[]}
*/
function unique(array) {
return Array.from(new Set(array))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment