← back to Exo
feat: keep-models option when uninstalling EXO (#1997)
667a3bb0e5e071c7ea3cc907a6ba32ae9b6fd27f · 2026-04-28 02:06:02 +0100 · Alex Cheema
## Summary
- Adds a **Keep downloaded models (~/.exo/models)** checkbox to the
macOS uninstall confirmation dialog (Settings → Advanced → Danger Zone).
The full `~/.exo` directory is now removed on uninstall by default; if
the checkbox is checked, `~/.exo/models` is preserved.
- The standalone `app/EXO/uninstall-exo.sh` gains a matching
`--keep-models` flag and the same `~/.exo` cleanup so GUI and CLI flows
stay in sync. Resolves the user home via `$SUDO_USER` since the script
runs under `sudo`.
Previously, "Uninstall EXO" only cleaned up system-level components
(LaunchDaemon, network location, logs, app bundle) and left the entire
`~/.exo` directory behind. Now uninstalling actually removes EXO's user
data, with a one-click opt-out for the (potentially many GB) of
downloaded models.

> Note: the rendered icon in the screenshot above is the generic system
folder icon because it was captured from a small standalone Swift binary
(no app bundle / icon resource). When triggered from the actual EXO.app,
the EXO app icon is shown.
## Test plan
- [ ] Build EXO.app locally; open Settings → Advanced → Danger Zone →
Uninstall EXO; confirm the new "Keep downloaded models (~/.exo/models)"
checkbox is present and unchecked by default.
- [ ] Uninstall with the checkbox **checked** → `~/.exo/models/`
survives, all other entries under `~/.exo` are gone, system components
removed, app moved to Trash.
- [ ] Uninstall with the checkbox **unchecked** → `~/.exo` is fully
removed.
- [ ] `sudo app/EXO/uninstall-exo.sh --keep-models` → `~/.exo/models/`
is preserved, the rest of `~/.exo` is removed.
- [ ] `sudo app/EXO/uninstall-exo.sh` (no flag) → `~/.exo` is fully
removed.
- [ ] `app/EXO/uninstall-exo.sh --help` prints usage and exits 0;
unknown args exit 2 with a usage hint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan <evanev7@gmail.com>
Files touched
M app/EXO/EXO/Views/SettingsView.swiftM app/EXO/uninstall-exo.sh
Diff
commit 667a3bb0e5e071c7ea3cc907a6ba32ae9b6fd27f
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Tue Apr 28 02:06:02 2026 +0100
feat: keep-models option when uninstalling EXO (#1997)
## Summary
- Adds a **Keep downloaded models (~/.exo/models)** checkbox to the
macOS uninstall confirmation dialog (Settings → Advanced → Danger Zone).
The full `~/.exo` directory is now removed on uninstall by default; if
the checkbox is checked, `~/.exo/models` is preserved.
- The standalone `app/EXO/uninstall-exo.sh` gains a matching
`--keep-models` flag and the same `~/.exo` cleanup so GUI and CLI flows
stay in sync. Resolves the user home via `$SUDO_USER` since the script
runs under `sudo`.
Previously, "Uninstall EXO" only cleaned up system-level components
(LaunchDaemon, network location, logs, app bundle) and left the entire
`~/.exo` directory behind. Now uninstalling actually removes EXO's user
data, with a one-click opt-out for the (potentially many GB) of
downloaded models.

> Note: the rendered icon in the screenshot above is the generic system
folder icon because it was captured from a small standalone Swift binary
(no app bundle / icon resource). When triggered from the actual EXO.app,
the EXO app icon is shown.
## Test plan
- [ ] Build EXO.app locally; open Settings → Advanced → Danger Zone →
Uninstall EXO; confirm the new "Keep downloaded models (~/.exo/models)"
checkbox is present and unchecked by default.
- [ ] Uninstall with the checkbox **checked** → `~/.exo/models/`
survives, all other entries under `~/.exo` are gone, system components
removed, app moved to Trash.
- [ ] Uninstall with the checkbox **unchecked** → `~/.exo` is fully
removed.
- [ ] `sudo app/EXO/uninstall-exo.sh --keep-models` → `~/.exo/models/`
is preserved, the rest of `~/.exo` is removed.
- [ ] `sudo app/EXO/uninstall-exo.sh` (no flag) → `~/.exo` is fully
removed.
- [ ] `app/EXO/uninstall-exo.sh --help` prints usage and exits 0;
unknown args exit 2 with a usage hint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan <evanev7@gmail.com>
---
app/EXO/EXO/Views/SettingsView.swift | 33 +++++++++++++++++++++---
app/EXO/uninstall-exo.sh | 49 +++++++++++++++++++++++++++++++++++-
2 files changed, 78 insertions(+), 4 deletions(-)
diff --git a/app/EXO/EXO/Views/SettingsView.swift b/app/EXO/EXO/Views/SettingsView.swift
index 52184e30..425cc760 100644
--- a/app/EXO/EXO/Views/SettingsView.swift
+++ b/app/EXO/EXO/Views/SettingsView.swift
@@ -552,15 +552,24 @@ struct SettingsView: View {
let alert = NSAlert()
alert.messageText = "Uninstall EXO"
alert.informativeText = """
- This will remove EXO and all its system components:
+ This will remove EXO and all its components:
• Network configuration daemon
• Launch at login registration
• EXO network location
+ • EXO data directory (~/.exo)
The app will be moved to Trash.
"""
alert.alertStyle = .warning
+
+ let checkbox = NSButton(
+ checkboxWithTitle: "Keep downloaded models (~/.exo/models)",
+ target: nil, action: nil)
+ checkbox.state = .off
+ checkbox.sizeToFit()
+ alert.accessoryView = checkbox
+
alert.addButton(withTitle: "Uninstall")
alert.addButton(withTitle: "Cancel")
@@ -570,11 +579,11 @@ struct SettingsView: View {
let response = alert.runModal()
if response == .alertFirstButtonReturn {
- performUninstall()
+ performUninstall(keepModels: checkbox.state == .on)
}
}
- private func performUninstall() {
+ private func performUninstall(keepModels: Bool) {
uninstallInProgress = true
controller.cancelPendingLaunch()
@@ -584,6 +593,7 @@ struct SettingsView: View {
DispatchQueue.global(qos: .utility).async {
do {
try NetworkSetupHelper.uninstall()
+ try Self.removeExoDirectory(keepModels: keepModels)
DispatchQueue.main.async {
LaunchAtLoginHelper.disable()
@@ -607,6 +617,23 @@ struct SettingsView: View {
}
}
+ private static func removeExoDirectory(keepModels: Bool) throws {
+ let fm = FileManager.default
+ let exoDir = ExoProcessController.exoDirectoryURL
+ guard fm.fileExists(atPath: exoDir.path) else { return }
+
+ if !keepModels {
+ try fm.removeItem(at: exoDir)
+ return
+ }
+
+ let contents = try fm.contentsOfDirectory(
+ at: exoDir, includingPropertiesForKeys: nil, options: [])
+ for entry in contents where entry.lastPathComponent != "models" {
+ try? fm.removeItem(at: entry)
+ }
+ }
+
private func moveAppToTrash() {
guard let appURL = Bundle.main.bundleURL as URL? else { return }
do {
diff --git a/app/EXO/uninstall-exo.sh b/app/EXO/uninstall-exo.sh
index d10b9fb3..b14490df 100755
--- a/app/EXO/uninstall-exo.sh
+++ b/app/EXO/uninstall-exo.sh
@@ -3,18 +3,41 @@
# EXO Uninstaller Script
#
# This script removes all EXO system components that persist after deleting the app.
-# Run with: sudo ./uninstall-exo.sh
+# Run with: sudo ./uninstall-exo.sh [--keep-models]
+#
+# Options:
+# --keep-models Preserve ~/.exo/models when removing the EXO data directory.
#
# Components removed:
# - LaunchDaemon: /Library/LaunchDaemons/io.exo.networksetup.plist
# - Network script: /Library/Application Support/EXO/
# - Log files: /var/log/io.exo.networksetup.*
# - Network location: "exo"
+# - EXO data directory: ~/.exo (or all of ~/.exo except models/ when --keep-models is set)
# - Launch at login registration
#
set -euo pipefail
+KEEP_MODELS=0
+for arg in "$@"; do
+ case "$arg" in
+ --keep-models)
+ KEEP_MODELS=1
+ ;;
+ -h | --help)
+ echo "Usage: sudo ./uninstall-exo.sh [--keep-models]"
+ echo " --keep-models Preserve ~/.exo/models when removing the EXO data directory."
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $arg" >&2
+ echo "Usage: sudo ./uninstall-exo.sh [--keep-models]" >&2
+ exit 2
+ ;;
+ esac
+done
+
LABEL="io.exo.networksetup"
# Current script path. Older installs used a different filename; keep the
# legacy path here so a fresh uninstall still cleans up upgraded machines.
@@ -25,6 +48,10 @@ LOG_OUT="/var/log/${LABEL}.log"
LOG_ERR="/var/log/${LABEL}.err.log"
APP_BUNDLE_ID="io.exo.EXO"
+# Resolve the invoking user's home, even when run via sudo.
+USER_HOME="$(eval echo "~${SUDO_USER:-$USER}")"
+EXO_DIR="$USER_HOME/.exo"
+
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
@@ -124,6 +151,22 @@ if networksetup -listnetworkservices 2>/dev/null | grep -q "Thunderbolt Bridge";
echo_info "Re-enabled Thunderbolt Bridge"
fi
+# Remove EXO data directory (~/.exo)
+EXO_DIR_REMOVED=""
+if [[ -d $EXO_DIR ]]; then
+ if [[ $KEEP_MODELS == "1" && -d "$EXO_DIR/models" ]]; then
+ find "$EXO_DIR" -mindepth 1 -maxdepth 1 ! -name models -exec rm -rf {} +
+ EXO_DIR_REMOVED="kept_models"
+ echo_info "Removed ~/.exo (preserved models/)"
+ else
+ rm -rf "$EXO_DIR"
+ EXO_DIR_REMOVED="full"
+ echo_info "Removed ~/.exo"
+ fi
+else
+ echo_warn "~/.exo not found (already removed?)"
+fi
+
# Note about launch at login registration
# SMAppService-based login items cannot be removed from a shell script.
# They can only be unregistered from within the app itself or manually via System Settings.
@@ -153,6 +196,10 @@ echo " • Network setup LaunchDaemon"
echo " • Network configuration script"
echo " • Log files"
echo " • 'exo' network location"
+case "$EXO_DIR_REMOVED" in
+full) echo " • EXO data directory (~/.exo)" ;;
+kept_models) echo " • EXO data directory (~/.exo, models preserved)" ;;
+esac
echo ""
echo "Your network has been restored to use the 'Automatic' location."
echo "Thunderbolt Bridge has been re-enabled (if present)."
← c80b10c0 implement engine abstraction for mlx and mflux (#2000)
·
back to Exo
·
feat(app): open Share Bug Report in a dedicated window (#200 1606e638 →