RecentDocs and MRUListEx: the RegRipper recentdocs plugin
8 min read
RecentDocs is the artifact that tells you a user opened a specific file through Explorer, and it is one of the cleaner file-access signals you get from a single hive. The catch is that the recency information lives in a binary MRUListEx blob that you have to decode by hand, and the only timestamp you get is the LastWrite of the key, which dates exactly one entry. The RegRipper recentdocs plugin does the decode for you, but you need to understand what it is and is not asserting before you put a RecentDocs line in a report. Treat RecentDocs as proof of file access plus ordering, and treat the timestamp as a single point, not a timeline.
The key and its per-extension subkeys
The data lives under HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs in the user's NTUSER.DAT. The layout is two-tiered:
- The top-level
RecentDocskey holds values for files of all types, in one combined most-recently-used list. - Underneath it, one subkey per file extension:
.docx,.pdf,.xlsx,.jpg, and so on, each holding the MRU for that one type. - A
Foldersubkey holds folder accesses rather than files.
Each key — the root and every extension subkey — has its own set of numbered values, its own MRUListEx, and its own LastWrite time. That last point matters and we will come back to it. The top-level key is not a master copy of the subkeys; an entry typically appears in both the type-specific subkey and the all-types root, with independent MRU positions in each.
The values themselves are named with plain integers: 0, 1, 2, 17, 42. The number is an opaque slot ID, not a rank and not a count. The ranking comes entirely from MRUListEx.
MRUListEx: the recency order
MRUListEx is a binary value whose job is to record the order in which the numbered slots were most recently used. It is an array of 4-byte little-endian integers, each one a slot ID, ordered most-recent-first, terminated by the sentinel 0xFFFFFFFF.
The decode is short. Walk the buffer four bytes at a time, read each as a little-endian uint32, and stop at 0xFFFFFFFF:
for (let i = 0; i + 4 <= raw.length; i += 4) {
const idx = dv.getUint32(i, true);
if (idx === 0xffffffff) break;
order.push(idx);
}
The resulting array is the MRU order. The first element is the slot that was opened most recently; the last is the oldest still tracked. To find the rank of a given numbered value, look up its slot ID in that array: rank 0 is the most recent. A value whose ID is not present in the decoded order has been dropped from the MRU and should be treated as stale or orphaned rather than as the current most-recent state.
The older MRUList (no Ex) used single ASCII characters as slot references. On any hive you are likely to touch in 2026 you will see MRUListEx with 4-byte integers. If you hit a hive old enough to use MRUList, the same most-recent-first logic applies, just with a different element width.
What is in the value data
Each numbered value is a binary structure that begins with the filename as a null-terminated UTF-16LE string, followed by a trailing PIDL — effectively an embedded LNK-style shell item reference to the same file.
For most timeline work you only need the leading filename. Decode the value as UTF-16LE and take the first null-terminated run:
new TextDecoder("utf-16le").decode(raw).split("\0").filter(Boolean)[0]
That yields the display name, for example Q2-budget.xlsx or incident-report.docx. The trailing PIDL is the same kind of structure that lives inside a .lnk file: it can carry the full shell path, the target's MFT reference, and other shell-item metadata. The recentdocs plugin in this parser reads only the leading UTF-16 name and leaves the PIDL alone; if you need the full path or the embedded file-reference data you parse the trailing shell item the same way you would parse a LNK file. Hedge accordingly: the bare RecentDocs name is a filename, not a guaranteed full path.
A decoded set of rows looks like this:
Type Name MRU rank Last opened (UTC)
.xlsx Q2-budget.xlsx 0 2026-06-15T09:14:22.000Z
.xlsx forecast-2025.xlsx 1
.docx incident-report.docx 0 2026-06-14T17:41:03.000Z
.pdf invoice-8841.pdf 0 2026-06-12T08:02:55.000Z
All Q2-budget.xlsx 0 2026-06-15T09:14:22.000Z
Note what carries a timestamp and what does not.
What it proves, and the one timestamp
When a name appears under RecentDocs in a user's NTUSER.DAT, it proves that user opened a file by that name through the Explorer shell — a double-click in a folder window, a pick from the Recent items jump list, a file opened from an Explorer-driven dialog. Because the data is in the per-user hive, attribution is unambiguous: the user who owns the profile is the user who opened the file.
Recency is where people overreach. There is no per-file timestamp anywhere in RecentDocs. The only time you get is the LastWrite of the key, and that LastWrite was set by the most recent write to that key — which is the operation that placed an entry at MRU rank 0. So the key's LastWrite dates the rank-0 entry and nothing else.
This parser encodes that limit directly: it emits the Last opened (UTC) value only for the row at MRU rank 0, and leaves the column blank for every other rank. The plugin note states it plainly — RecentDocs has no per-file timestamp; the key's last-write time dates only its most-recently-used entry. That is the honest representation. A tool that stamps every RecentDocs row with the same key LastWrite is asserting a timestamp it does not have, and you should not trust output that does it.
The per-extension layout gives you more rank-0 timestamps than the root alone would. Because each extension subkey and the Folder subkey keeps its own MRUListEx and its own LastWrite, you get one reliable timestamp per type: the most recent .docx open, the most recent .pdf open, the most recent folder access, each dated independently. That is genuinely useful — it is a set of "last time this user touched a file of type X via Explorer" points — but it is still one point per key, not a history.
Limits and pivots
The structural limit is the timestamp. RecentDocs gives you ordering for everything in the MRU and a real time for one entry per key. If you need the access time of rank 2 in a .docx subkey, RecentDocs cannot give it to you. You infer ordering and pivot for timing.
The behavioural limit is the same as for most shell artifacts: RecentDocs records Explorer-driven file opens. A file read by a script, opened by a command-line tool, or touched by a service does not necessarily land here. RecentDocs is a window into deliberate, GUI-driven user behaviour, which is exactly why it is good for attribution and bad as a complete file-access log.
The pivots that turn a single RecentDocs name into a timed, corroborated event:
- comdlg32 OpenSavePidlMRU. The common Open/Save dialog keeps its own MRU under
ComDlg32\OpenSavePidlMRU, broken out by extension, recording files chosen in open/save dialogs across applications. It overlaps RecentDocs but is populated by a different code path, so an entry in one and not the other tells you how the file was reached. See the comdlg32 plugin notes for the layout and the same MRUListEx decode. - LNK files in
Recent\. Explorer drops a.lnkin%APPDATA%\Microsoft\Windows\Recent\for files it tracks. Those shortcuts carry real timestamps — target created/modified/accessed — plus volume serial and MFT reference. The trailing PIDL inside the RecentDocs value is the same shell-item data in a more complete form. Parse the LNK file to get the times RecentDocs withholds.
The standard sequence: read the name and MRU rank from RecentDocs, confirm the open-dialog path through OpenSavePidlMRU, then resolve the actual timestamps and full target path from the matching LNK in Recent\. That reconstructs "this user opened this exact file at this time" with attribution from the hive and timing from the shortcut.
Tools
- RegRipper's
recentdocsplugin. Walks theRecentDocskey and every extension subkey, decodesMRUListEx, and prints entries in MRU order. The classic RegRipper output groups by key and lists the LastWrite per key. Read the plugin source to confirm exactly which fields it surfaces. - Eric Zimmerman's RECmd with a RecentDocs batch file produces the same data in CSV.
- The parser on this site decodes RecentDocs in the tree view: it resolves MRUListEx to a rank, pulls the leading UTF-16 name, and — correctly — dates only the rank-0 entry per key. You can analyze NTUSER.DAT in your browser without uploading the hive anywhere.
For the broader set of NTUSER plugins this one sits alongside, see the RegRipper plugins reference.
The bottom line
RecentDocs answers "did this user open a file by this name, and in what order relative to the others." It answers "when" for exactly one entry per key. The MRUListEx blob is what makes the ordering recoverable, and the per-extension subkeys multiply your single reliable timestamp into one-per-type. Decode the blob, respect the single-timestamp limit, and pivot to OpenSavePidlMRU and the LNK files in Recent\ for the timing RecentDocs does not keep. Used that way it is a sharp tool. Used as if every row carried the key's LastWrite, it is a way to put a wrong time in a report.