DefaultAppCtl is a macOS command-line tool for applying default application handler mappings for:
- URL schemes (e.g.,
http,mailto,webcal) - UTType / UTI identifiers (e.g.,
com.adobe.pdf,public.html)
It is designed for enterprise deployment via a signed PKG and supports execution in both:
- root context (e.g., PKG
postinstall) - logged-in user GUI context (required for reliable per-user LaunchServices defaults)
To make user‑context execution reliable and support delayed application installs, this project also includes a user-facing CLI wrapper:
defaultappctl-user— a helper that waits for dependencies, retries as needed, and runs explicitly as the logged-in user.
If you are setting the default browser (http / https):
- End-user GUI context is required
- Defaults must be applied while logged in as the user
- This typically means:
- The end user runs
defaultappctl-user, or - A helpdesk / imaging technician runs it while logged in
- The end user runs
There is no supported or reliable way to silently force the default browser ahead of a user session.
No workaround was found to bypass this limitation.
If you discover a supported method around this, please share it.
There are three primary ways to use this project.
Which one you choose depends on:
- Your MDM (Intune, Jamf, etc.)
- Whether you need to set the default browser
- Whether you want zero user interaction
This method supports default browsers, delayed installs, and real-world app timing.
-
Deploy the PKG via Intune
-
Let the postinstall run (this installs
defaultappctl-user) -
While logged in as the user, run:
defaultappctl-user apply
This can be done by:
- The end user (with instructions)
- Helpdesk staff
- Imaging / provisioning teams
-
Deploy the PKG via Jamf
-
Choose one of the following:
- Embed
postinstall.intuneinto the PKGpostinstall, or - Run
postinstall.intuneas a Jamf policy script
If you encounter timing or context issues when running it as a separate script, embedding it directly in
postinstallis recommended. - Embed
-
While logged in as the user, run:
defaultappctl-user apply
Useful for testing, break-glass, or ad-hoc provisioning.
-
Host the PKG on an internal or public web server
-
As the logged-in user:
curl -L -o DefaultAppCtl.pkg https://example.com/DefaultAppCtl.pkg sudo installer -pkg DefaultAppCtl.pkg -target / defaultappctl-user applyIf you know the dependencies are installed then you can just run the package normally without the installer command.
If you do not need to set the default browser, this method is preferred.
- Deploy the PKG via MDM
- Create a user-context script that contains the logic from the embedded
postinstall - Run it automatically in the user context
This ensures:
- No user prompts
- No manual execution
- Fully automated deployment
- The embedded
postinstalldoes not wait for dependencies - For initial provisioning:
- Add dependency checks,
- Delay execution until apps are installed or
- Have the script run repeatedly
- In Intune or Jamf, you can expose the PKG via:
- Company Portal (Intune)
- Self Service (Jamf)
The sections below are intended for administrators or developers who want to modify, extend, or deeply understand how DefaultAppCtl works.
- Package identifier:
com.yourorg.defaultappctl
src/DefaultAppCtl.swift— Swift CLI toolresources/defaults.json— default mappings (strict JSON)pkg_scripts/postinstall— base installer scriptpkg_scripts/postinstall.intune— installs the user CLI wrapperbuild_pkg.sh— builds the.pkg.work/— build staging/output
- macOS 12+ at runtime
- Xcode Command Line Tools (for
swiftc) to build pkgbuild(included with macOS developer tools)
Do NOT use
sudoto build.
zsh ./build_pkg.sh
If .work is owned by root from a previous build:
sudo rm -rf .work
Build log:
.work/build.log
- Binary:
/usr/local/bin/defaultappctl - Config:
/Library/Application Support/DefaultAppCtl/defaults.json
- Binary:
/usr/local/bin/defaultappctl-user - Man page:
/usr/local/share/man/man1/defaultappctl-user.1
- User runs:
/var/tmp/defaultappctl.<uid>.manual.user.log - State:
/var/tmp/defaultappctl.<uid>.manual.user.state.json
defaultappctl --apply --mode root|user --config <path> --state <path> --log <path>
Example:
sudo /usr/local/bin/defaultappctl \
--apply --mode root \
--config "/Library/Application Support/DefaultAppCtl/defaults.json" \
--state "/Library/Application Support/DefaultAppCtl/state.json" \
--log "/var/log/defaultappctl.log"
Strict JSON (no comments).
Example:
{
"urls": {
"mailto": "com.microsoft.Outlook",
"http": "com.google.Chrome"
},
"types": {
"com.adobe.pdf": "com.adobe.Acrobat.Pro"
}
}
macOS maintains default handlers differently for root vs logged-in users.
--mode rootis best-effort--mode useris authoritative
The defaultappctl-user wrapper exists to make user-mode execution safe and repeatable.
0— success20— failures in root mode21— failures in user mode10— macOS too old11— invalid arguments12— config decode failure
tail -n 200 /var/log/defaultappctl.log
cat "/Library/Application Support/DefaultAppCtl/state.json"
Bundle ID validation:
mdfind "kMDItemCFBundleIdentifier == 'com.google.Chrome'" | head
- No network access
- No shelling out from Swift
- Uses bundle identifiers
- Best-effort logging
- Deterministic application order
This project uses @main and must be compiled with:
-parse-as-library
The provided build_pkg.sh enforces this flag.