Safari History.db forensics
2026-06-21 · 3 min
Safari's History.db is a plain SQLite database. Two tables carry the
timeline — history_items holds one row per URL, history_visits holds
one row per visit — and you join them on history_item = id. Get the join
and the timestamp epoch right and the rest is straightforward.
Where it lives
/Users/<user>/Library/Safari/History.db
/Users/<user>/Library/Safari/History.db-wal
/Users/<user>/Library/Safari/History.db-shm
Always grab the -wal and -shm sidecars alongside the main file. Recent
visits sit in the write-ahead log before they are checkpointed into the
database; acquire History.db alone and you miss them. See
Safari history file locations for
the rest of the Safari artifact set.
history_items
One row per distinct URL. The columns that matter:
| Column | Meaning |
|---|---|
id | Primary key — joined by history_visits.history_item |
url | The full URL |
domain_expansion | The host, used for autocomplete |
visit_count | Total visits across all time |
daily_visit_counts | Per-day visit tallies (blob) |
This is the aggregated view. For timeline work you want the visits table, not these rolled-up counts.
history_visits
One row per individual visit. This is where the timeline lives:
| Column | Meaning |
|---|---|
id | Visit primary key |
history_item | FK → history_items.id |
visit_time | Mac absolute time of the visit (float64) |
title | Page title at visit time |
load_successful | 0 if the load failed |
http_non_get | Non-GET request flag |
redirect_source | Visit id this visit was redirected from |
redirect_destination | Visit id this visit redirected to |
origin | How the visit was initiated |
To get a usable record — URL, title, and time together — join the two tables:
SELECT i.url, v.title, v.visit_time, v.load_successful
FROM history_visits v
JOIN history_items i ON i.id = v.history_item
ORDER BY v.visit_time;
Timestamps
visit_time is Mac absolute time — seconds (often fractional, stored
as float64) since 2001-01-01 00:00:00 UTC. Add the offset to reach Unix:
unix_seconds = visit_time + 978307200
It is UTC, with no embedded timezone. Keep the timeline in UTC and label any local rendering. See browser timestamp formats for the full epoch reference.
Following redirects
redirect_source and redirect_destination reference other rows in
history_visits by id. Walk the chain to reconstruct how a user landed
on a page — a shortener, an ad click, an SSO bounce — rather than treating
each row as an independent visit. This distinguishes a deliberate
navigation from a passive redirect, which matters for intent.
load_successful and friends
load_successful = 0 flags a visit that never completed — a typo, a dead
link, a blocked host. Failed loads still prove the URL was requested,
which is often the point. Pair this with http_non_get to spot form
submissions and other non-GET activity.
Acquisition notes
- Full Disk Access is required for a user-mode tool to read Safari's data; otherwise image the volume or use a FileVault-aware acquisition.
- Capture the
-walsidecar for the most recent visits — they may not be in the main file yet. - BrowserForensics parses
History.dbdirectly in the browser (URL, title,visit_count,visit_time), all client-side, so the database never leaves the analyst's machine. - Cross-reference with cookies for the same hosts; see Safari binarycookies format.