Skip to content

QuarantineEventsV2: macOS Download History Forensics

What the macOS QuarantineEventsV2 database records: LSQuarantineEvent schema, Mac absolute time, type numbers, the xattr UUID join and its limits.

Published on 7 min read

TL;DR. QuarantineEventsV2 is a per-user SQLite database in ~/Library/Preferences/ where macOS records downloads made by apps that opt in to quarantine, such as Safari, Chrome, Mail, Messages and AirDrop. Its single table, LSQuarantineEvent, tells you when (in Mac absolute time), which app, from which URL and gives an event UUID that also ends the downloaded file's com.apple.quarantine attribute. It does not store the file name, and it never sees what curl, wget or git fetched.

What QuarantineEventsV2 is

When an app that participates in quarantine saves something from the network, macOS tags the file with the quarantine attribute and, for many of these downloads, writes an event to the user's QuarantineEventsV2 database. The file lives here, one per user account:

~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2

Rows typically survive after the downloaded file is deleted, and no automatic purge is documented. The database therefore often describes downloads whose files are long gone, which is what makes it a download history in practice. Rows can still be deleted with sqlite3, and the whole file can be removed.

The LSQuarantineEvent schema

This is the schema observed on a macOS 26.6 system. Apple does not document the columns; the descriptions below follow the column names and observed data.

ColumnTypeWhat it holds
LSQuarantineEventIdentifierTEXT, primary key, not nullEvent UUID; also the last field of the file's quarantine attribute
LSQuarantineTimeStampREALEvent time in Mac absolute time
LSQuarantineAgentBundleIdentifierTEXTBundle identifier of the downloading app
LSQuarantineAgentNameTEXTName of the downloading app
LSQuarantineDataURLStringTEXTURL of the downloaded data
LSQuarantineSenderNameTEXTSender name, when the item came from a person
LSQuarantineSenderAddressTEXTSender address, under the same condition
LSQuarantineTypeNumberINTEGEREvent type number (see below)
LSQuarantineOriginTitleTEXTTitle of the originating page
LSQuarantineOriginURLStringTEXTPage or location the download started from
LSQuarantineOriginAliasBLOBBinary alias data

Two indexes exist: LSQuarantineEventIndex on the identifier and LSQuarantineTimeStampIndex on the timestamp. On that system the database used rollback-journal mode (journal_mode=delete) with a page size of 4096 bytes, so expect a -journal companion more often than a -wal, but collect both.

Timestamps: Mac absolute time

LSQuarantineTimeStamp counts seconds since 2001-01-01 00:00:00 UTC, which is Mac absolute time. Add 978307200 to get a Unix timestamp. The column is declared REAL, but SQLite may store a REAL whose value is a whole number as an integer on disk, a documented storage optimisation. A parser that only accepts 8-byte floats will misread or skip such rows; it has to accept both encodings.

LSQuarantineTypeNumber

Apple's public header LSQuarantine.h lists six type constants in this order: WebDownload, OtherDownload, EmailAttachment, InstantMessageAttachment, CalendarEventAttachment, OtherAttachment. Mapping them to 0 through 5 in that order is the commonly reported interpretation; it is not documented by Apple.

ValueReported meaningObserved on macOS 26.6
0WebDownloadChrome and Homebrew Cask downloads
1OtherDownload
2EmailAttachment
3InstantMessageAttachmentMessages
4CalendarEventAttachment
5OtherAttachment
6Undocumentedsharingd (AirDrop)
7UndocumentedA Safari row

Treat the number as a hint and rely on the agent columns to say which app was involved.

Observations from a live macOS 26.6 system

The following come from one test system. They are observations, not rules, but they explain a lot of confusing output:

  • Many rows, notably from Chrome and Messages, had no URL column filled at all.
  • Safari downloads carried a UUID in their quarantine attribute but had no matching database row in recent months.
  • Some attribute values were 0081;00000000;;: no time, no agent, no UUID.
  • Agent names in the attribute escape spaces as \x20, for example Microsoft\x20Word.
  • A few UUIDs were lower-case.

The lesson: an unmatched UUID or an empty URL is common and not suspicious on its own.

What a row proves, and what it does not

A live row shows that an app which opts in to quarantine registered a download or attachment event, for this user account, at that time, with those URLs and that agent. Joined to a file's attribute, it ties a file on disk to a source.

It does not show that the file still exists, that it was opened or executed, or who was at the keyboard; a stolen session produces the same rows as the legitimate user. It does not name the local file. And the absence of a row is weak evidence: downloads by tools that do not opt in (curl, wget, scp, rsync, git) never appear, rows can be deleted with sqlite3, and the file itself can be deleted.

Querying it with sqlite3

Work on a copy, never on the original (opening a database can modify it; see recovering deleted records). This query lists events in time order with a UTC date:

SELECT datetime(LSQuarantineTimeStamp + 978307200, 'unixepoch') AS event_utc,
       LSQuarantineAgentBundleIdentifier, LSQuarantineDataURLString,
       LSQuarantineOriginURLString, LSQuarantineEventIdentifier
FROM LSQuarantineEvent ORDER BY LSQuarantineTimeStamp;

Joining events to files through the xattr UUID

The database knows URLs but not files; the file knows its event but not always its URL. The link is the UUID. A quarantine attribute looks like this:

0083;66f7c2a1;Safari;6E4C2B1A-...

The last field matches LSQuarantineEventIdentifier. Compare case-insensitively, since some UUIDs are stored in lower case. One event can map to several files: Archive Utility copies the archive's quarantine value, UUID included, onto every file it extracts. The flag field and the other parts of the value are covered in com.apple.quarantine flags, and the browser-set URL attribute in kMDItemWhereFroms vs quarantine.

How Quarantine Parser reads the database

Quarantine Parser runs entirely in your browser (Rust compiled to WebAssembly in a Web Worker); nothing is uploaded. It uses its own read-only SQLite reader rather than a SQLite library, which matters in three ways:

  • WAL. A -wal file is applied the way SQLite does it: header and frame checksums and salts are verified, and only committed frames are used. A -journal companion is read too.
  • Partial copies. Truncated, partial or locked copies are still read as far as they go, instead of failing on the first error.
  • Recovery. Rows that are no longer live are carved out of free space in table pages, freelist pages, older page copies, superseded or uncommitted WAL frames and rollback-journal pages. See recovering deleted QuarantineEventsV2 records.

You can drop several users' databases at once, including Aftermath's lsquarantine_<user> copies, together with xattr listings. The Timeline, Agents, Domains and Files views then show each event with the files that carry its UUID, and the Findings view lists leads, not verdicts.

Frequently asked questions

Where is the QuarantineEventsV2 database on macOS?

Each user has one at ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2. It is a SQLite database with a single table, LSQuarantineEvent. Copy it together with any -wal or -journal file sitting next to it.

Does QuarantineEventsV2 record the name of the downloaded file?

No. It stores URLs, the downloading app, a timestamp and an event UUID, but not the local path or file name. To tie an event to a file, match the UUID at the end of the file's com.apple.quarantine attribute.

How do I convert LSQuarantineTimeStamp to a date?

It is Mac absolute time: seconds since 2001-01-01 00:00:00 UTC. Add 978307200 to get Unix time, for example with datetime(LSQuarantineTimeStamp + 978307200, 'unixepoch') in sqlite3.

Does an empty QuarantineEventsV2 mean nothing was downloaded?

No. Tools such as curl, wget, scp, rsync and git do not opt in to quarantine, rows can be deleted with sqlite3 and the whole file can be removed. Absence is weak evidence.

Related articles