Skip to content
Home » How to Read and Convert plist Files on macOS (plutil)

How to Read and Convert plist Files on macOS (plutil)

Dark terminal card showing the command file sample.plist bin.plist and its output, sample.plist identified as XML 1.0 document text ASCII text and bin.plist as Apple binary property list, under the heading Half your Mac's config files aren't text, with three labels reading plutil -p reads both, -extract Key raw for scripts, and -lint names the line

Property lists hold most of the configuration on a Mac — app preferences, launchd jobs, bundle Info.plist files, the OS version itself. Open one in a text editor and roughly half the time you get a screen of binary garbage, because macOS stores plists in two interchangeable formats and prefers the compact one. plutil is the built-in tool that reads both, converts between them, pulls single values out for scripts, and tells you exactly which line of a hand-edited file you broke. Everything below was run on macOS 26.5.2 against throwaway files.

Read any plist, binary or XML

The two formats are xml1, which is readable text, and binary1, which is not. file tells them apart:

$ file sample.plist bin.plist
sample.plist: XML 1.0 document text, ASCII text
bin.plist:    Apple binary property list

plutil -p prints either one the same way, so you never have to care which you have:

$ plutil -p bin.plist
{
  "Label" => "com.example.backup"
  "ProgramArguments" => [
    0 => "/usr/bin/rsync"
    1 => "-a"
  ]
  "RunAtLoad" => true
  "StartInterval" => 3600
}

Arrays are printed with their indexes, which is genuinely useful — those indexes are the key paths you use later to extract or replace a single element. What this output is not is a data format. The man page is blunt about it: “The output format is not stable and not designed for machine parsing. The purpose of this command is to be able to easily read the contents of a plist file, no matter what format it is in.” If you find yourself piping plutil -p into grep and awk, jump to the extract section instead.

Convert between the formats

-convert rewrites the file in place. To make a binary plist editable in a text editor:

plutil -convert xml1 bin.plist        # binary -> readable XML, in place
plutil -convert binary1 sample.plist  # back to binary, in place

The size difference is real but small at this scale — the same four keys were 429 bytes as XML and 155 bytes as binary. Binary wins on parse speed more than on disk, which is why the system uses it for files that get read constantly.

Add -o to write somewhere else instead of overwriting, and -o - to send the result to standard output so you can pipe it. A dash as the input file reads from standard input, so plutil -convert xml1 -o - - works as a filter in the middle of a pipeline. That pair is the one habit worth forming: without -o, -convert silently replaces the original file, which is fine for a copy and annoying for anything you cared about.

Where the JSON conversion stops

-convert json looks like the obvious way to get a plist into jq, and for simple files it is. Add -r for indented output:

$ plutil -convert json -r -o - sample.plist
{
  "Label" : "com.example.backup",
  "ProgramArguments" : [
    "\/usr\/bin\/rsync",
    "-a"
  ],
  "RunAtLoad" : true,
  "StartInterval" : 3600
}

The catch is that plists have two types JSON does not: date and data. Real system plists are full of both — timestamps, icon blobs, serialized objects. A file containing either one refuses to convert:

$ plutil -convert json -o - typed.plist
typed.plist: Invalid object in plist for JSON format
$ echo $?
1

It fails cleanly with a non-zero exit rather than dropping the offending keys, which is the right behaviour but easy to miss in a script that does not check. If you hit this, convert to xml1 and parse that, or extract the specific keys you actually want.

Pull one value out for a script

-extract takes a key path and an output format. raw gives you the bare value with no quotes or wrapper, which is what you want inside a shell script:

$ plutil -extract Label raw -o - sample.plist
com.example.backup

$ plutil -extract ProgramArguments.0 raw -o - sample.plist
/usr/bin/rsync

Key paths use dots for both dictionary keys and array indexes, so ProgramArguments.0 is the first element of that array. This is the one-liner for reading the OS version out of a system file, and it works on the binary original with no conversion step:

$ plutil -extract ProductVersion raw -o - /System/Library/CoreServices/SystemVersion.plist
26.5.2

A key path that does not exist is an error, not an empty string — so a script can branch on the exit status instead of guessing whether a blank line meant “absent” or “empty”:

$ plutil -extract Nope raw -o - sample.plist
sample.plist: Could not extract value, error: No value at that key path or invalid key path: Nope
$ echo $?
1

Change values in place

Three commands cover editing, and each takes the same key paths as -extract. New values need a type flag — -string, -integer, -bool, -float, -date, -data, -array, -dictionary:

plutil -insert Nice -string veryhigh edit.plist    # add a key that isn't there
plutil -replace StartInterval -integer 900 edit.plist
plutil -remove RunAtLoad edit.plist

-insert and -replace are not synonyms, and the difference is enforced. Inserting over a key that already exists fails instead of overwriting it:

$ plutil -insert Label -string dup e2.plist
e2.plist: Value already exists at key path Label
$ echo $?
1

That is a good default for automation: a script that means “add this” fails loudly rather than quietly clobbering a value someone else set. Editing also preserves the file’s existing format — a binary plist edited with -replace is still a binary plist afterwards, so you do not have to convert to XML and back to change one number.

Check a file you hand-edited

-lint parses the file and says nothing useful when it is fine, which is the point:

$ plutil -lint sample.plist
sample.plist: OK

When it is not fine you get the reason and the line number, which is the whole reason to run it before handing a plist to launchd or an installer:

$ plutil -lint broken.plist
broken.plist: (Value missing for key inside <dict> at line 5)
$ echo $?
1

A <key> with no value after it is the classic hand-editing mistake, and it is exactly the kind of thing a LaunchAgent will refuse to load over with a far less specific complaint. If you write launchd job files by hand, lint is the step to put in front of loading the job.

One oddity worth knowing: plutil -p on a broken file reports two failures, because it tries the file as JSON after the plist parse fails.

$ plutil -p broken.plist
broken.plist: Property List error: Value missing for key inside <dict> at line 5 / JSON error: JSON text did not start with array or object and option to allow fragments not set. around line 1, column 0.
$ echo $?
1

The JSON half is noise. Read the first half and ignore the rest, or use -lint, which only reports the one that matters.

plutil or defaults?

Both tools read and write plists, and most tutorials treat them as interchangeable. They are not. defaults is for preference domains — it talks to the preferences system by bundle identifier, and the file on disk is an implementation detail. plutil operates on files, whatever they are and wherever they live.

Apple has said which way this is going. man defaults carries this warning:

WARNING: The defaults command will be changed in an upcoming
major release to only operate on preferences domains. General
plist manipulation utilities will be folded into a different
command-line program.

So for a bundle’s Info.plist, a launchd job file, a configuration profile, or anything else that is a plist but not a preference domain, plutil is the tool that is meant to keep doing the job.

The reverse direction has a caveat that is worth stating carefully, because the internet is confident about it and the confidence outruns the evidence. The standard advice is that editing a preferences file directly is pointless, since the preferences daemon caches values in memory and will write its own copy back over yours. On macOS 26.5.2, with a throwaway domain that no running app had open, that did not happen: a value changed on disk with plutil -replace was returned by defaults read immediately, both before and after warming the cache with a read. The real hazard is narrower than the folklore — it is an app that is currently running and holding its own preferences in memory, which writes them out when it quits and undoes your edit. That is a reason to quit the app first, not a reason to believe direct edits never take. For preference domains, defaults write is still the tool that avoids the question entirely.

Also worth noting: man plutil is 151 lines and mentions preferences, the preferences daemon, or defaults exactly zero times. Nothing in the documentation for the file-level tool warns you that a whole class of plists is managed by something else. That gap is where most of the confusion between these two commands comes from.

Command quick reference

GoalCommand
Read a plist of any formatplutil -p file.plist
Make a binary plist text-editableplutil -convert xml1 file.plist
Convert without touching the originalplutil -convert xml1 -o out.plist file.plist
Print as JSON to stdoutplutil -convert json -r -o - file.plist
Read one value for a scriptplutil -extract KeyPath raw -o - file.plist
Add a key that does not exist yetplutil -insert Key -string value file.plist
Overwrite an existing keyplutil -replace Key -integer 900 file.plist
Delete a keyplutil -remove Key file.plist
Check syntax before loadingplutil -lint file.plist

The short version: -p to look, -extract … raw to script against, -lint before anything loads the file, and -o whenever you would rather not overwrite what you started with.

Leave a Reply

Your email address will not be published. Required fields are marked *