The RecentFileCache.bcf file format, in plain terms
The BCF format is a header, a list of length-prefixed UTF-16LE paths, and that's it. Here is what every byte means and why nothing has changed since 2009.
There are forensic file formats that fight you. CBS logs, ESE databases, the Windows event log binary structure. RecentFileCache.bcf is not one of them. It is a header, a counter, and a list of strings. You can parse it in twelve lines of Python and you will not need a hex editor for any of it.
That simplicity is the reason the format has not changed since Windows 7 SP1 shipped. There is nothing to extend. Microsoft moved on to the AmCache instead of versioning this file.
What is in the file
C:\Windows\AppCompat\Programs\RecentFileCache.bcf is written by the Application Experience appraisal task on Win7 SP1 and Server 2008 R2. The on-disk layout, in order:
- A fixed header. Twenty bytes.
- A 32-bit little-endian count of entries.
- Zero or more entries, each one being a length-prefixed UTF-16LE string holding the full NT path of an executable.
That is the entire file. No checksums. No CRC. No tail. No per-entry timestamp. No per-entry hash. If the file is 18 bytes long, it has a header and nothing else. If it is two megabytes long, it has been growing for a while because the appraiser has not run.
The header
The first twenty bytes are constant on every BCF I have ever pulled off a Win7 host. Five 32-bit little-endian values, all of them magic constants. The interesting one is the first: 0xFE 0xFF 0xEE 0xFE. The rest are zero or padding. If you see a different value in the first four bytes, you do not have a RecentFileCache.bcf. You have something else that happens to share the name.
I have seen exactly one case of a corrupted header in the wild, and it was caused by an antivirus product half-quarantining the file. The header bytes had been replaced with zeros. Worth checking.
The entry count
Bytes 20 through 23, little-endian, are the count of entries that follow. If this number is zero, the appraiser ran recently and cleared the working set. If it is large, either nothing has cleared the file in weeks, or a lot of new binaries have shown up between appraisal passes. Both are interesting in their own way.
The count is authoritative. If the count says ten and the file contains eleven strings, treat the eleventh as junk. In practice this does not happen because the file is written atomically by a single process, but parsers should not trust string-walking alone to find the end.
The entries
Each entry is:
- A 32-bit little-endian length in bytes (not characters).
- That many bytes of UTF-16LE string data.
The strings are NT object manager paths. They start with \??\ and then a normal Windows path:
\??\C:\Windows\System32\rundll32.exe
\??\C:\Users\bob\AppData\Local\Temp\setup.exe
Treat the \??\ prefix as C:\. It is the kernel's way of saying "DOS device namespace" and it carries no forensic meaning beyond "this is a regular file path." Strip it for display, keep it for fidelity if you are exporting machine-readable output.
The length is in bytes, which trips up almost everybody on a first parse. A path with thirty Unicode characters takes sixty bytes plus whatever the file uses for the trailing null, which is typically two bytes but you should not depend on it. Read the length, read that many bytes, decode as UTF-16LE, strip trailing nulls. Done.
There is no separator between entries. The next entry's length field starts immediately after the previous entry's string data. If your parser drifts by a byte, you will get garbage for the rest of the file. This is why honoring the entry count matters: it gives you a stop condition that does not depend on the string content.
Twelve lines of Python
import struct
def parse_bcf(path):
with open(path, "rb") as f:
data = f.read()
if data[:4] != b"\xfe\xff\xee\xfe":
raise ValueError("not a RecentFileCache.bcf")
count = struct.unpack_from("<I", data, 20)[0]
offset = 24
entries = []
for _ in range(count):
length = struct.unpack_from("<I", data, offset)[0]
offset += 4
s = data[offset:offset + length].decode("utf-16-le").rstrip("\x00")
entries.append(s)
offset += length
return entries
That is enough to parse any BCF you will encounter. No dependencies. No edge cases I have hit in production. If you are doing this in a corporate environment where you cannot run arbitrary Python on the evidence machine, the parser on this site does the same thing in the browser with no upload.
What the format does not tell you
This is the part where the format's simplicity becomes a limitation. The BCF does not record:
- Execution time. You know the binary ran since the last appraisal pass, and that is it.
- Execution count. One run and a hundred runs look identical.
- The user who ran it. The file is per-machine.
- A hash. You cannot pivot to threat intel from the file's contents.
- A load list. Unlike Prefetch, there is no record of DLLs loaded into the process.
- Command line arguments. None. Not even the existence of arguments.
If you need any of those, pair the BCF with Prefetch, the Master File Table, the USN journal, and whatever EVTX you have. The BCF tells you what; everything else tells you when, how, and who.
Why the format has not changed
Microsoft replaced this file with the AmCache in Windows 8 and never went back to add fields to the BCF. The AmCache is an ESE-style hive with dozens of fields per entry: hashes, file metadata, install time, publisher information. The BCF was never going to grow into that. It existed as a working file for an appraisal pipeline that has since been redesigned around different primitives.
The practical consequence is that a 2014 parser and a 2026 parser produce the same output on the same input. There is no version drift to worry about. Tools written before the format was publicly documented still work. The original public analysis by Willi Ballenthin at Mandiant in 2014 is still accurate today; I have not seen a single byte deviate from what he documented.
This is rare for Windows artifacts. Most of them mutate across builds. Shimcache changed five times between Win7 and Win10. The AmCache schema gained and lost columns. Prefetch got compressed in Windows 10. The BCF just sits there, frozen, because nobody had a reason to touch it after Win7.
A note on truncation
After the appraiser processes the file, the OS truncates it back to "header plus zero count." The file does not get deleted. The file system metadata (creation time, parent directory entry) survives. This matters for two reasons:
- The created time of
RecentFileCache.bcfitself can predate every path inside it by months or years. Do not use the file's own timestamps to bound the entries. - A binary that ran two days ago and was processed by the appraiser yesterday is gone from the file, but the file is still there, looking innocent and small. Empty is not the same as never-used.
Further reading
- Willi Ballenthin, RecentFileCache.bcf. The original public format analysis. Still correct.
- Microsoft, Application Experience and Program Inventory. Vendor context for why the file exists.
- Yogesh Khatri, AmCache and friends. For when you need to know what came next.
If you have parsed any binary forensic format before, the BCF will feel like a warm-up exercise. That is the right reaction. Spend your effort on triaging the path list, not on the file format.