Skip to content

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.

Published on 6 min read

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:

  1. A fixed header. Twenty bytes.
  2. A 32-bit little-endian count of entries.
  3. 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.bcf itself 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

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.

Related articles

A hands-on read of the RecentFileCache.bcf binary format with a hex dump, a decoded record, and the edge cases a parser has to handle.
The BCF's lifecycle is one scheduled task. Understand when ProgramDataUpdater runs, when it clears the file, and how to spot a disabled appraiser.
A SOC-flagged Win7 endpoint, 14 paths in the BCF, three worth a second look. A realistic corroboration chain and how much the BCF actually contributed.