Unpicking Apple's Software Update Catalog Files
Introduction to the sucatalog file
For many years, Apple have published software update product information via “sucatalog” files. These files have traditionally been used by the macOS system for information on available updates, including by the softwareupdate
command. These catalog files have included information on many types of software updates, including “full” macOS installer applications, core apps, iMovie and iWork apps, plugins and drivers, and many more.
I don’t pretend to be an expert on the history of these files, but it seems from their naming that the concept started around the time of macOS 10.5, or Leopard. Here’s the URL of the latest sucatalog
file that is used on macOS 15 systems:
I can only speculate as to how this URL evolved, but it’s been consistent for many major OS releases now, as this compilation of the past few shows:
https://swscan.apple.com/content/catalogs/others/index-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
https://swscan.apple.com/content/catalogs/others/index-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
https://swscan.apple.com/content/catalogs/others/index-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
https://swscan.apple.com/content/catalogs/others/index-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
https://swscan.apple.com/content/catalogs/others/index-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
https://swscan.apple.com/content/catalogs/others/index-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
It’s also interesting that each catalog is maintained going all the way back to Leopard, I suspect because the name is baked into each macOS major version’s system somewhere. Indeed, the OG catalog https://swscan.apple.com/content/catalogs/others/index-leopard.merged-1.sucatalog is still being served, and when I looked at it while writing this post, was updated today, though the most recent software update listed in the file is for the Motion app, posted October 23, 2019.
It’s quite well known that there are catalogs containing information about beta releases of Apple Software, too. Typically, the URL for these “seed” catalogs includes an extra layer after index-
and before the first macOS version, matching that version and adding seed
or beta
, referring (I think) to AppleSeed and Developer Seed releases. For example, the URL for software updates relating to the macOS 15 AppleSeed program are in the following URL:
https://swscan.apple.com/content/catalogs/others/index-15seed-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog
The URL for software updates relating to macOS Tahoe developer program is as follows:
https://swscan.apple.com/content/catalogs/others/index-26beta-26-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz
Diving into the files
The sucatalog
files are sometimes served as gzipped files, as we saw above. That’s helpful, because they are very large files. That first sucatalog file for Leopard is currently 1 MB in size. The gzipped “26beta” sucatalog file is 508 KB, but unzips to 7 MB. What’s inside them to make them so large? Put simply, a lot of different software updates are still available… That latter catalog has 98,229 lines!
Let’s look briefly at what’s inside. Despite the sucatalog
file suffix, these files are, as often with Apple, Property List files (PLIST) - an XML-compliant format. They consist of just four top-level keys:
CatalogVersion
(always2
)ApplePostURL
(always an empty value)IndexDate
(in the format2025-08-26T21:31:52Z
, and seemingly being updated daily)Products
.Products
is a dictionary of available products.
Each entry in the Products
dictionary consists of the following keys:
- A product ID, in the form of a 3-digit number, followed by a dash, followed by a four- or five-digit number. For example,
061-1955
refers to a DVD Player update from 2005, and093-27146
refers to an XProtect Plist Configuration Data release from 26 August, 2025. ServerMetadataURL
, a URL for a metadata file ending in.smd
.Packages
, an array consisting of a dictionary of entries, each of which contains aURL
key, the value of which is the URL of the available.pkg
file for this product; aMetadataURL
key, which links to a.pkm
file, which is an XML file with some interesting metadata regarding the package, such as its version string (CFBundleShortVersionString
); the packageSize
; and aDigest
value (I’m going to guess this is a hash value).PostDate
, when this product was posted.Distributions
, a dictionary listing any number of languages with URLs pointing to.dist
files, which also provide additional information about the product.
Products which are macOS full installers have additional keys in their Products
dictionary:
DeferredSUEnablementDate
, a date that you would imagine would be some date later than the post date, but I can’t confirm that from the entries I took a look at.ExtendedMetaInfo
, which is added in place ofServerMetadataURL
for these entries, and contains a dictionary consisting of one entry,InstallAssistantPackageIdentifiers
, itself a dictionary of bundle identifiers for components of the full installer package.
Without delving forever further into the contents of the file, I’m now going to concentrate only on the full macOS installers that are listed in the Products
dictionary. Firstly, how do we identify that the product is a full macOS installer?
Until 2020, I think up until Monterey - the final Intel-only release - the release included a DMG installer, BaseSystem.dmg
, amongst other files. The InstallAssistantPackageIdentifiers
dictionary contained an OSInstall
key (com.apple.mpkg.OSInstall
), and an InstallInfo
key (com.apple.plist.InstallInfo
).
From Big Sur onwards, this all changed as Apple Silicon devices came into play. The release started to include an InstallAssistant.pkg
package, which contains the full installer application, as well as packages relating to firmware and installer information.
Using information from sucatalog files to download full macOS installers
As the main author of erase-install, a script for downloading full macOS installers and using them to update, reinstall or erase macOS systems, I’ve been using other Mac Admin’s projects to read the sucatalog files. Originally, I adapted Greg Neagle’s installinstallmacos.py python script for this purpose, and latterly - when python stopped being bundled with macOS by default - switched to using Nindi Gill’s [mist-cli] swift command line tool to do it. I also included an alternative method, which uses the built in softwareupdate --list-full-installers
and softwareupdate --fetch-full-installer
commands, though these commands have had their issues over the years so never commanded the same level of confidence as the third party tools.
I’ve started looking into the sucatalog files myself because mist-cli
is currently having an issue with obtaining the installers on systems running macOS 15.6 or newer, and I didn’t want users of the script to be left in the lurch. The issue with mist-cli
is not related to any change to the contents of the sucatalog files, but rather due to some change in the way the installer
binary handles the .dist
files that are obtained using the URLs in the .sucatalog
files (or, I should say, how it doesn’t handle them any more…).
Both mist-cli
and installinstallmacos.py
use these .dist
files to compile an distribution package which consists of not just the InstallAssistant.pkg
package, but other items which have not proved necessary (as far as I can tell) for the purposes of using the full installers in the way that erase-install
uses them. Indeed, to solve the issue compiling these packages on newer systems, Greg Neagle has made an update to installinstallmacos.py
which instead just takes the InstallAssistant.pkg
package and nothing else.
Faced with potentially pivoting back to bundling in a python distribution with erase-install and using my fork of installinstallmacos.py
once again, I first wanted to see if I could build a way of obtaining the same information as mist-cli
and installinstallmacos.py
using only built-in commands.
Going down the rabbit hole
This process has been helped somewhat by the addition of jq
to macOS from version 15 onwards, but we also have PlistBuddy
, plutil
and xmllint
to help us, as well as sed
, awk
, grep
, etc.
The information I required was as follows:
- Product ID - easy, we have this in the
sucatalog
file. - URL of the
InstallAssistant.pkg
- also easy, it’s right there too. - Package size - we have this also.
- Release date - also there.
- Title of the release (e.g. macOS Sequoia)
- Version String (e.g. 15.6.1)
- Build String (e.g. 24G90)
- Compatibility information (which models the package will successfully install on)
For those last four items, we need to grab the Distribution file. So we need to iterate through the huge list of products in the sucatalog
files, filter only those which contain an InstallAssistant.pkg
URL, and grab the associated URL of the .dist
file.
I first tried converting the entire sucatalog PLIST to JSON using pliutil
, but this fails, as there are some keys that are not convertable to JSON.
Next, I tried extracting each Product
dict using plutil
, but this took forever so was unusable in practise.
The fastest method I found was to use PlistBuddy
to obtain a list of Products. It’s a bit hacky, but much faster than plutil
, as follows:
products=$(/usr/libexec/PlistBuddy -c "Print :Products:" "$catalog_plist_path" | sed -n 's/^ \([0-9]\{3\}-[0-9]\{5\}\) = Dict {$/\1/p')
Using this list, I then tried to use plutil
to convert each individual product dictionary to JSON:
package_plist=$(plutil -extract Products."$ia_product".Packages json -o - "$catalog_plist_path" 2>/dev/null)
This failed for no real reason - seems to be a bug. I was instead able to extract the individual product dict as XML1 (i.e. PLIST), and then convert it to JSON. Shrug emoji.
package_plist=$(plutil -extract Products."$ia_product".Packages xml1 -o - "$catalog_plist_path" 2>/dev/null)
package_json=$(echo "$package_plist" | plutil -convert json -o - - 2>/dev/null)
OK, now we can iterate through all the products, identify those that are macOS full installers, and grab the InstallAssistant.pkg
URL and package size:
ia_url=$(jq -r 'to_entries | map(select(.value.URL and (.value.URL | endswith("InstallAssistant.pkg")))) | .[0].value.URL // empty' <<< "$package_json" 2>/dev/null)
pkg_size_bytes=$(jq -r 'to_entries | map(select(.value.URL and (.value.URL | endswith("InstallAssistant.pkg")))) | .[0].value.Size // empty' <<< "$package_json" 2>/dev/null)
ia_pkg_size=$(bc <<< "scale=2; $pkg_size_bytes / 1024 / 1024 / 1024")
We can also grab the Post Date, and the .dist
file URL from the Product dict, again using plutil
because we couldn’t convert to JSON at that level:
ia_post_date=$(plutil -extract Products."$ia_product".PostDate raw -o - "$catalog_plist_path" 2>/dev/null)
ia_post_date=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ia_post_date" "+%Y-%m-%d" 2>/dev/null)
dist_file=$(plutil -extract Products."$ia_product".Distributions.English raw -o - "$catalog_plist_path" 2>/dev/null)
These .dist
files are in XML format, where we can use xmllint
to obtain the Display Name, Version String and Build String. The compatibility information is slightly more difficult, because it’s contained in a C script embedded in the XML, so needs parsing with grep
, sed
, and tr
(or awk
). Note that Intel Macs have a “Board ID” with which you can test compatibility, whereas Apple Silicon Mac has a “Device ID”:
ia_title=$(xmllint --xpath 'string(/installer-gui-script/title/text())' "$dist_xml" 2>/dev/null)
ia_build=$(xmllint --xpath "string(//dict/string[preceding-sibling::key[1]='BUILD']/text())" "$dist_xml" 2>/dev/null)
ia_version=$(xmllint --xpath "string(//dict/string[preceding-sibling::key[1]='VERSION']/text())" "$dist_xml" 2>/dev/null)
ia_supportedBoardIDs=$(grep "var supportedBoardIDs" "$dist_xml" 2>/dev/null | sed -E 's/.*\[\s*([^]]+)\].*/\1/' | tr -d "'")
ia_supportedDeviceIDs=$(grep "var supportedDeviceIDs" "$dist_xml" 2>/dev/null | sed -E 's/.*\[\s*([^]]+)\].*/\1/' | tr -d "'")
Finally, all the relevant information about each full macOS installer can be compiled into a nice JSON file, feeding the original products
list from the first PlistBuddy
command into an elaborate jq
command:
jq -n \
--arg product "$ia_product" \
--arg post_date "$ia_post_date" \
--arg url "$ia_url" \
--arg title "$ia_title" \
--arg build "$ia_build" \
--arg version "$ia_version" \
--arg pkg_size "$ia_pkg_size" \
--arg compatible "$ia_compatible" \
--argjson supported_board_ids "$(echo "$ia_supportedBoardIDs" | jq -R 'split(",") | map(ltrimstr(" ") | rtrimstr(" "))')" \
--argjson supported_device_ids "$(echo "$ia_supportedDeviceIDs" | jq -R 'split(",") | map(ltrimstr(" ") | rtrimstr(" "))')" \
'{
product: $product,
post_date: $post_date,
url: $url,
title: $title,
build: $build,
version: $version,
pkg_size: $pkg_size,
compatible: $compatible,
supported_board_ids: $supported_board_ids,
supported_device_ids: $supported_device_ids
}' >> "$tmp_json_file"
And we can combine all the individual bits of JSON that were outputted to the tmp_json_file
into one valid blob of JSON with the following “slurp” command:
jq -s '[.[] | select(type == "object")]' "$tmp_json_file" > "$installers_list_json_file"
Finally, for listing purposes, we can output to the terminal in a readable format with a bit more jq
trickery:
echo "┌────────────┬──────────────────┬─────────┬──────────┬──────────┬────────────┬────────────┐"
echo "│ PRODUCT ID │ TITLE │ VERSION │ BUILD │ SIZE GB │ DATE │ COMPATIBLE │"
echo "├────────────┼──────────────────┼─────────┼──────────┼──────────┼────────────┼────────────┤"
jq -r 'sort_by(.version | split(".") | map(tonumber)) | reverse | .[] | [
(.product // ""),
(.title // ""),
(.version // ""),
(.build // ""),
(.pkg_size // ""),
(.post_date // ""),
(.compatible // "")
] | @tsv' "$installers_list_json_file" | while IFS=$'\t' read -r ia_product ia_title ia_version ia_build ia_size ia_date ia_compatible; do
printf "│ %-10s │ %-16s │ %-7s │ %-8s │ %-8s │ %-10s │ %-10s │\n" \
"$ia_product" "$ia_title" "$ia_version" "$ia_build" "$ia_size" "$ia_date" "$ia_compatible"
done
echo "└────────────┴──────────────────┴─────────┴──────────┴──────────┴────────────┴────────────┘"
We get something like this:
┌────────────┬──────────────────┬─────────┬──────────┬──────────┬────────────┬────────────┐
│ PRODUCT ID │ TITLE │ VERSION │ BUILD │ SIZE GB │ DATE │ COMPATIBLE │
├────────────┼──────────────────┼─────────┼──────────┼──────────┼────────────┼────────────┤
│ 093-26142 │ macOS Tahoe Beta │ 26.0 │ 25A5349a │ 15.69 │ 2025-08-25 │ True │
│ 093-10756 │ macOS Sequoia │ 15.6.1 │ 24G90 │ 14.58 │ 2025-08-20 │ True │
│ 082-08661 │ macOS Sequoia │ 15.6 │ 24G84 │ 14.58 │ 2025-08-05 │ True │
│ 082-44432 │ macOS Sequoia │ 15.5 │ 24F74 │ 14.57 │ 2025-05-19 │ True │
│ 082-33200 │ macOS Sequoia │ 15.4.1 │ 24E263 │ 14.53 │ 2025-04-23 │ True │
│ 082-16524 │ macOS Sequoia │ 15.4 │ 24E248 │ 14.53 │ 2025-03-31 │ True │
│ 082-04099 │ macOS Sequoia │ 15.3.2 │ 24D2082 │ 12.14 │ 2025-03-20 │ False │
│ 082-01336 │ macOS Sequoia │ 15.3.2 │ 24D81 │ 14.20 │ 2025-03-20 │ True │
│ 072-70706 │ macOS Sequoia │ 15.3.1 │ 24D70 │ 14.20 │ 2025-02-17 │ True │
│ 093-22120 │ macOS Sonoma │ 14.7.8 │ 23H730 │ 12.71 │ 2025-08-20 │ True │
│ 082-85709 │ macOS Sonoma │ 14.7.7 │ 23H723 │ 12.71 │ 2025-08-05 │ True │
│ 082-42388 │ macOS Sonoma │ 14.7.6 │ 23H626 │ 12.72 │ 2025-05-19 │ True │
│ 082-11498 │ macOS Sonoma │ 14.7.5 │ 23H527 │ 12.71 │ 2025-04-18 │ True │
│ 072-84039 │ macOS Sonoma │ 14.7.4 │ 23H420 │ 12.71 │ 2025-02-17 │ True │
│ 093-22004 │ macOS Ventura │ 13.7.8 │ 22H730 │ 11.36 │ 2025-08-20 │ True │
│ 082-87267 │ macOS Ventura │ 13.7.7 │ 22H722 │ 11.36 │ 2025-08-05 │ True │
│ 082-42293 │ macOS Ventura │ 13.7.6 │ 22H625 │ 11.35 │ 2025-05-19 │ True │
│ 082-11327 │ macOS Ventura │ 13.7.5 │ 22H527 │ 11.36 │ 2025-04-18 │ True │
│ 072-83845 │ macOS Ventura │ 13.7.4 │ 22H420 │ 11.36 │ 2025-02-17 │ True │
│ 052-60131 │ macOS Monterey │ 12.7.4 │ 21H1123 │ 11.55 │ 2024-03-18 │ True │
│ 042-45246 │ macOS Big Sur │ 11.7.10 │ 20G1427 │ 11.56 │ 2023-09-11 │ True │
└────────────┴──────────────────┴─────────┴──────────┴──────────┴────────────┴────────────┘
So we can do this with built-in tools, but should we?
The answer is probably not. There may be faster ways to do the parsing described above than I was able to achieve in this exercise, but it is much slower at getting the results than either mist-cli
or installinstallmacos.py
, though it is comparable in speed to softwareupdate --list-full-installers
.
Could we instead use Apple’s GDMF feed (gdmf.apple.com/v2/pmv
), which is what the SOFA project does - or indeed, use the SOFA feed directly? Yes, except that the GDMF feed (and by extension, the SOFA feed) doesn’t contain any beta installer information, which has been a valuable functionality of erase-install
, to me at least, so I didn’t want to lose that.
Conclusion
The latest release of erase-install, version 38.0, includes the code above to obtain a list of installers when running on macOS 15.6 or newer. It downloads only the InstallAssistant.pkg
for the relevant installer, directly installing the full installer application into the Applications
folder when using the --reinstall
or --erase
options. The added bonus of this in comparison to how it worked with mist-cli
and installinstallmacos.py
is that no extra drive space is required to create a disk image containing the installer, saving around 15 GB of temporary space requirement during the download and install process.
You can still use the --fetch-full-installer
option. On macOS older than 15.6, you can use the “native” mode described above by adding the --native
flag - but apart from the space considerations, there’s really no need. Once Nindi fixes mist-cli
, I will probably revert to using it due to its faster speed, though if anyone has any good ideas on how to speed up the process with built-in tools, please let me know or file a PR!
One final note, I used LLMs quite extensively in pursuit of getting the correct jq
commands. jq
is incredibly powerful but it is its own language, and the LLMs know that language much better than me. However, it frequently makes up commands and requires very firm direction and extensive testing of every line.