Archaeology

Saved Application State

Much of this discussion is based on reverse-engineering of file formats and frameworks, but we haven't bothered to pepper it with qualifiers. Since our reverse-engineering skills are not beyond reproach, and macOS is always changing, a grain of salt is advised. If you have corrections to any details, please do get in touch.

What Is A savedState Directory?

The savedState directories are where macOS stores information for the “Resume” feature — that is, the preservation of state when quitting an app (i.e. System Settings > Desktop & Dock > Windows & Apps > Close windows when quitting an application is disabled; or on macOS 12, System Preferences > General > Close windows when quitting an app) or when restarting your computer (i.e. in the restart confirmation dialog, Reopen windows when logging back in is checked).

The files in savedState are created and used by AppKit.framework, and by talagent(8), which is the background agent that manages state saving and restoration. [As noted in the man page, TAL stands for transparent app lifecycle, the original name for “Resume.” This was a reference to the never-quite-realized idea that the user should never (need to) know whether or not an app is running or has been silently quit by macOS. This is perhaps one of the earliest iOS-ification efforts on macOS, dating all the way back to Mac OS X 10.7 (Lion).]

A savedState directory contains a few different types of files, each explained below. But first, a digression into where to find these directories on modern macOS.

Where To Find savedState Directories?

Through macOS 14 (Sonoma), directories with the savedState extension were found under your home folder at Library/Saved Application State — or for a sandboxed application, under the app's “container” at under Library/Containers/com.example.bundle-identifier/Data/Library/Saved Application State.

Starting in macOS 15 (Sequoia), the savedState directories are managed by a new daemon, /System/Library/CoreServices/talagentd. The state for all apps is inside the talagentd container, which lives under Library/Daemon Containers (inside your home folder). Since these “daemon containers” are identified by UUID, you have to use the (hidden) .com.apple.containermanagerd.metadata.plist file inside each container to determine which one belongs to talagentd, i.e.:

$ cd ~/Library/Daemon\ Containers
$ grep talagent */.com.apple.containermanagerd.metadata.plist # note "talagent", not "talagentd"
Binary file 531BF3F7-C43C-492E-8D7E-655D9DD8979F/.com.apple.containermanagerd.metadata.plist matches
$ cd 531BF3F7-C43C-492E-8D7E-655D9DD8979F/Data/Library/Saved\ Application\ State

Within this Saved Application State directory, you'll find all of the app-specific savedState directories. These are also named by UUID, so you'll need to examine the ApplicationMapping.plist in this directory to determine which UUID corresponds to which app. (Note that the root of this property list is an array, and the UUID comes after the dictionary describing the app.)

The Library/Daemon Containers directory is protected by TCC (presumably, the reason that saved application state was moved here). As such, you will probably need to grant Full Disk Access to Terminal in order to find and access the savedState directories at all.

windows.plist

The windows.plist contains the top-level metadata about each of the (restorable) windows of the app, along with some app-level information. Note that there will always be a window defined for the main menu bar, since that's also drawn and managed in a window.

The contents of this property list are mostly self-explanatory, but one key is non-obvious: NSDataKey contains a per-window encryption key that is used to AES-128-CBC encrypt the archived data that is stored in the data.data file.

Props to Sector 7 for discovering and documenting this critical point about NSDataKey, in their writeup of CVE-2021-30873. We'd tried to decode this state previously, but found it was encrypted, and it never ocurred to us that maybe the encryption key was sitting right next to the ciphertext in plain sight!

data.data

The data.data file is the core of the restorable state data. This file is a binary, append-only list of records, each one containing:

  1. the window ID that the record applies to;
  2. a string identifier, which is the NSUserInterfaceItemIdentifier for an NSResponder that was asked to encode its restorable state; and
  3. a data blob, which the NSKeyedArchiver result of the responder's -encodeRestorableStateWithCoder: implementation.

Parts (b) and (c) of each data.data record are further encoded into another custom key-value pair structure, and encrypted with the window's NSDataKey, using AES-128-CBC (and thus will be padded to the 128-bit block size).

More specifically, each record starts with a header akin to this:

struct PersistentUIRecordHeader
{
    char        _magic[ 8 ];    // "NSCR1000"
    uint32_t    _windowID;      // applicable window ID [BE]
    uint32_t    _length;        // total length of record, including this header [BE]
};

The window ID and all of the length fields (above and below) are Big Endian.

The ( _length - sizeof( struct PersistentUIRecordHeader ) ) bytes after the header are the ciphertext of the record. Once this has been decrypted with the NSDataKey, it has a structure like this:

struct PersistentUIRecordPlaintext
{
    uint32_t    _unknown;               // usually but not always zero
    uint32_t    _keyLength;             // length of the key string which follows [BE]
    char        _key[ _keyLength ];     // the UTF-8 key string
    char        _separator;             // "rchv"
    uint32_t    _dataLength;            // length of the archived data which follows [BE]
    uint8_t     _data[ _dataLength ];   // the NSKeyedArchiver-encoded data
};

As explained above, _key is the NSUserInterfaceItemIdentifier and _data is the -encodeRestorableStateWithCoder: data blob.

Because responders can be invalidated in any combination at any time, new records are added to data.data without deleting or rewriting previous ones, so only the last record for a given NSUserInterfaceItemIdentifier should be considered applicable. (At some point, talagent does clear out and rewrite the file, and encryption keys do get refreshed on some schedule, but we haven't investigated that any further.)

window_%u.data

For each window, there might also be a “snapshot” for the app window: this is actually a screenshot of the window as it last existed, which is sometimes used after restart to “fake” immediate restoration while the real work takes place.

The window screenshots live in files named window_%u.data, where the uint32_t suffix is the corresponding window ID. The contents of this file is:

  1. a raw CGImage backing buffer;
  2. that has been compressed using the zlib DEFLATE algorithm; and then
  3. AES-128-CBC-encrypted.

However, the last encryption step is not done with the NSDataKey, as with the data.data. Instead, this encryption uses a key that is chosen randomly by talagent and stored in your keychain. (You can find this key in Keychain Access, by searching for an item named Apple Persistent State Encryption.) The key appears to get cycled by talagent on some regular basis.

Revision History

Revision history for “Saved Application State” page
DateChanges
June 19, 2025 Added information about where to find the savedState directories in macOS 15 and beyond.
March 9, 2023 Original version published.