Clean Uninstall Dropbox on macOS and Reset File Provider

How I traced Dropbox filesystem changes on a clean macOS VM and made a dry run first script to remove Dropbox local state, caches, services, and File Provider leftovers.

While testing multiple Dropbox accounts on macOS at the same time through Parall, I needed one Dropbox instance to run without File Provider.

That should have been a temporary setup choice. Instead, I ran into a Dropbox bug. After File Provider was disabled, I could not turn it back on again from the app.

That sent me into the local Dropbox state on macOS. I wanted to understand what had to be removed before Dropbox would behave like a fresh install again.

While looking into that, I noticed something bigger. Dropbox creates a lot of files and folders that most users never see. If someone drags Dropbox to the Trash, many local pieces can remain behind.

So I decided to make a script that removes Dropbox completely from macOS. As a side effect, it also fixed the File Provider issue for me.

How I traced the files

To avoid guessing, I started with a clean macOS virtual machine in UTM.

First I dumped the filesystem structure before Dropbox was installed. Then I installed Dropbox, logged in, authorized it, and dumped the filesystem structure again.

That gave me two snapshots to compare.

After analyzing the difference, I had a concrete list of files and folders created by Dropbox. I used that list to build the uninstall script.

The script is dry run first. It stops Dropbox services, checks whether Dropbox processes are still running, removes local Dropbox state, removes caches and temporary files, and gives options for what to preserve.

By default it keeps the main Dropbox app bundle. It also keeps AppleScript based actions disabled unless you explicitly enable them, because those can require additional macOS permissions.

The script

The latest version is in the GitHub repository ReverseEverything/DropboxCleanUninstallMacOS.

The best way to use it is to download the script from GitHub, because that is where I will keep the latest version.

If you copy the script from this post into a local .sh file, save it as uninstall-dropbox-complete.sh, then make it executable.

chmod +x uninstall-dropbox-complete.sh

First run a dry run and read what it plans to remove.

./uninstall-dropbox-complete.sh --user "$USER"

When the dry run looks correct, run the cleanup.

sudo ./uninstall-dropbox-complete.sh --apply --user "$USER"

If you want to remove the main Dropbox app bundle too, add --remove-app-bundle.

sudo ./uninstall-dropbox-complete.sh --apply --user "$USER" --remove-app-bundle

If you want to keep your local synced Dropbox folder, add --keep-synced-content.

sudo ./uninstall-dropbox-complete.sh --apply --user "$USER" --keep-synced-content

The script tries to stop Dropbox services first. If Dropbox is still running, it will stop and ask you to quit the remaining Dropbox processes in Activity Monitor before it continues.

You can copy the script from the block below.

#!/usr/bin/env bash
#
# Dropbox uninstall cleanup for macOS
# Made by Ighor July for https://reverseeverything.com
# GitHub repository https://github.com/ReverseEverything/DropboxCleanUninstallMacOS
#
# Dry run is the default. Pass --apply to delete files.
# /Applications/Dropbox.app is kept by default.
# Pass --remove-app-bundle to remove the main app bundle.
# AppleScript is disabled by default.
# Pass --use-osascript to allow AppleScript based quit and login item cleanup.

set -u
set -o pipefail

apply=0
target_user=""
target_home=""
keep_synced_content=0
remove_app_bundle=0
use_osascript=0
remove_audit_side_effects=0
audit_list=""

usage() {
  cat <<'EOF'
Usage
  sudo bash uninstall-dropbox-complete.sh --apply --user admin

Dry run
  bash uninstall-dropbox-complete.sh --user admin

Options
  --apply
      Delete files. Without this flag the script only prints actions.

  --user NAME
      Remove Dropbox data for this macOS user.

  --keep-synced-content
      Keep ~/Dropbox and ~/Library/CloudStorage/Dropbox.

  --remove-app-bundle
      Also remove /Applications/Dropbox.app.
      The app bundle is kept by default.

  --use-osascript
      Use AppleScript to ask Dropbox to quit and to remove login items.
      This is disabled by default because macOS may require extra permissions.

  --audit-list PATH
      Optional path list from the filesystem audit.
      Only Dropbox named paths from this list are removed.

  --remove-audit-side-effects
      Also remove safe Dropbox related side effects from the audit list.
      This does not delete Apple system asset stores or shared databases.
EOF
}

while [ "$#" -gt 0 ]; do
  case "$1" in
  --apply)
    apply=1
    ;;
  --user)
    shift
    if [ "$#" -eq 0 ]; then
      echo "Missing value for --user" >&2
      exit 2
    fi
    target_user="$1"
    ;;
  --keep-synced-content)
    keep_synced_content=1
    ;;
  --remove-app-bundle)
    remove_app_bundle=1
    ;;
  --use-osascript)
    use_osascript=1
    ;;
  --audit-list)
    shift
    if [ "$#" -eq 0 ]; then
      echo "Missing value for --audit-list" >&2
      exit 2
    fi
    audit_list="$1"
    ;;
  --remove-audit-side-effects)
    remove_audit_side_effects=1
    ;;
  -h | --help)
    usage
    exit 0
    ;;
  *)
    echo "Unknown option $1" >&2
    usage >&2
    exit 2
    ;;
  esac
  shift
done

script_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)"
if [ -z "$audit_list" ] && [ -f "$script_dir/dropbox-added-normalized-paths-without-audit.txt" ]; then
  audit_list="$script_dir/dropbox-added-normalized-paths-without-audit.txt"
fi

detect_user() {
  if [ -n "$target_user" ]; then
    return
  fi

  if [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER:-}" != "root" ]; then
    target_user="$SUDO_USER"
    return
  fi

  local console_user
  console_user="$(/usr/bin/stat -f %Su /dev/console 2>/dev/null || true)"
  if [ -n "$console_user" ] && [ "$console_user" != "root" ]; then
    target_user="$console_user"
    return
  fi

  target_user="${USER:-}"
}

detect_home() {
  target_home="$(/usr/bin/dscl . -read "/Users/$target_user" NFSHomeDirectory 2>/dev/null | /usr/bin/awk '{print $2}' || true)"
  if [ -z "$target_home" ]; then
    target_home="/Users/$target_user"
  fi
}

detect_user
if [ -z "$target_user" ]; then
  echo "Could not determine target user. Pass --user NAME" >&2
  exit 2
fi

detect_home
if [ ! -d "$target_home" ]; then
  echo "Target home does not exist. $target_home" >&2
  exit 2
fi

target_uid="$(/usr/bin/id -u "$target_user" 2>/dev/null || true)"
if [ -z "$target_uid" ]; then
  echo "Could not determine uid for $target_user" >&2
  exit 2
fi

if [ "$(id -u)" -ne 0 ]; then
  echo "Run with sudo for full removal. Dry run still works without sudo." >&2
fi

if [ "$apply" -eq 0 ]; then
  echo "Dry run mode. Re-run with --apply to delete."
fi

echo "Target user $target_user"
echo "Target home $target_home"

print_cmd() {
  printf '+'
  printf ' %q' "$@"
  printf '\n'
}

run_cmd() {
  print_cmd "$@"
  if [ "$apply" -eq 1 ]; then
    "$@"
  fi
}

try_cmd() {
  print_cmd "$@"
  if [ "$apply" -eq 1 ]; then
    "$@" || true
  fi
}

run_as_user() {
  if [ "$(id -u)" -eq 0 ]; then
    try_cmd /bin/launchctl asuser "$target_uid" /usr/bin/sudo -u "$target_user" "$@"
  else
    try_cmd "$@"
  fi
}

path_exists() {
  [ -e "$1" ] || [ -L "$1" ]
}

remove_path() {
  local path="$1"

  case "$path" in
  "" | "/" | "/Applications" | "/Library" | "/System" | "/Users" | "/private" | "/private/var" | "/private/var/folders" | "$target_home" | "$target_home/Library")
    echo "Skip unsafe path $path" >&2
    return
    ;;
  esac

  if path_exists "$path"; then
    run_cmd /bin/rm -rf "$path"
  fi
}

rmdir_empty() {
  local path="$1"
  case "$path" in
  "" | "/" | "/Applications" | "/Library" | "/System" | "/Users" | "/private" | "/private/var" | "$target_home" | "$target_home/Library")
    return
    ;;
  esac

  if [ -d "$path" ]; then
    try_cmd /bin/rmdir "$path"
  fi
}

remove_glob() {
  local path
  for path in "$@"; do
    remove_path "$path"
  done
}

unload_launch_items() {
  local item

  for item in \
    "$target_home/Library/LaunchAgents/com.dropbox.DropboxUpdater.wake.plist" \
    "$target_home/Library/LaunchAgents/com.dropbox.dropboxmacupdate.agent.plist" \
    "$target_home/Library/LaunchAgents/com.dropbox.dropboxmacupdate.xpcservice.plist"; do
    if path_exists "$item"; then
      try_cmd /bin/launchctl bootout "gui/$target_uid" "$item"
    fi
  done

  for item in \
    /Library/LaunchDaemons/com.dropbox.DropboxMacUpdate.agent.plist \
    /Library/LaunchDaemons/com.dropbox.dropboxmacupdate.agent.plist \
    /Library/LaunchDaemons/com.getdropbox.dropbox.UpdaterPrivilegedHelper.plist; do
    if path_exists "$item"; then
      try_cmd /bin/launchctl bootout system "$item"
    fi
  done
}

stop_dropbox() {
  if [ "$use_osascript" -eq 1 ]; then
    run_as_user /usr/bin/osascript -e 'tell application "Dropbox" to quit'
  else
    echo "Skipping AppleScript quit command. Pass --use-osascript to enable it."
  fi

  /bin/sleep 2

  try_cmd /usr/bin/pkill -x Dropbox
  try_cmd /usr/bin/pkill -f 'Dropbox Helper'
  try_cmd /usr/bin/pkill -f DropboxMacUpdate
  try_cmd /usr/bin/pkill -f DropboxUpdateClient
  try_cmd /usr/bin/pkill -f DropboxSoftwareUpdate
  try_cmd /usr/bin/pkill -f com.getdropbox.dropbox
  try_cmd /usr/bin/pkill -f com.dropbox
}

list_dropbox_processes() {
  {
    /usr/bin/pgrep -fl 'Dropbox' 2>/dev/null || true
    /usr/bin/pgrep -fl 'Dropbox Helper' 2>/dev/null || true
    /usr/bin/pgrep -fl 'DropboxMacUpdate' 2>/dev/null || true
    /usr/bin/pgrep -fl 'DropboxUpdateClient' 2>/dev/null || true
    /usr/bin/pgrep -fl 'DropboxSoftwareUpdate' 2>/dev/null || true
    /usr/bin/pgrep -fl 'com.getdropbox.dropbox' 2>/dev/null || true
    /usr/bin/pgrep -fl 'com.dropbox.' 2>/dev/null || true
    /usr/bin/pgrep -fl 'G7HH3F8CAK.com.getdropbox.dropbox.sync' 2>/dev/null || true
  } | /usr/bin/sort -u | /usr/bin/awk '
    $0 ~ /uninstall-dropbox-complete[.]sh/ { next }
    $0 ~ /[p]grep -fl/ { next }
    $0 ~ /[a]wk / { next }
    { print }
  '
}

ensure_no_dropbox_processes() {
  if [ "$apply" -eq 0 ]; then
    echo "Skipping live process guard in dry run mode."
    return
  fi

  local running
  local reply

  while true; do
    running="$(list_dropbox_processes || true)"
    if [ -z "$running" ]; then
      return
    fi

    echo "Dropbox related processes are still running."
    echo "$running"
    echo "Quit these processes in Activity Monitor, then press Return to check again."
    echo "Type q and press Return to abort."

    if [ ! -t 0 ]; then
      echo "Cannot continue in a non interactive shell while Dropbox processes are running." >&2
      exit 1
    fi

    read -r reply
    case "$reply" in
    q | Q | quit | QUIT)
      echo "Aborted because Dropbox processes are still running." >&2
      exit 1
      ;;
    esac
  done
}

reset_shared_registrations() {
  local bundle

  for bundle in \
    com.getdropbox.dropbox \
    com.getdropbox.dropbox.fileprovider \
    com.getdropbox.dropbox.TransferExtension \
    com.getdropbox.dropbox.garcon \
    com.dropbox.client.crashpad \
    G7HH3F8CAK.com.getdropbox.dropbox.sync; do
    if command -v tccutil >/dev/null 2>&1; then
      run_as_user /usr/bin/tccutil reset All "$bundle"
      if [ "$(id -u)" -eq 0 ]; then
        try_cmd /usr/bin/tccutil reset All "$bundle"
      fi
    fi

    if command -v pluginkit >/dev/null 2>&1; then
      run_as_user /usr/bin/pluginkit -r -i "$bundle"
      if [ "$(id -u)" -eq 0 ]; then
        try_cmd /usr/bin/pluginkit -r -i "$bundle"
      fi
    fi
  done

  if [ "$use_osascript" -eq 1 ]; then
    run_as_user /usr/bin/osascript -e 'tell application "System Events" to delete every login item whose name contains "Dropbox"'
  else
    echo "Skipping AppleScript login item cleanup. Pass --use-osascript to enable it."
  fi
}

remove_core_paths() {
  if [ "$remove_app_bundle" -eq 1 ]; then
    remove_path /Applications/Dropbox.app
  else
    echo "Keeping /Applications/Dropbox.app. Pass --remove-app-bundle to delete it."
  fi

  if [ "$keep_synced_content" -eq 0 ]; then
    remove_path "$target_home/Dropbox"
    remove_path "$target_home/Library/CloudStorage/Dropbox"
  fi

  remove_path "$target_home/.dropbox"
  remove_path "$target_home/.dropbox-master"
  remove_path "$target_home/.dropbox-dist"

  remove_path "$target_home/Library/Application Support/Dropbox"
  remove_path "$target_home/Library/Dropbox"
  remove_path "$target_home/Library/Application Support/FileProvider/com.getdropbox.dropbox.fileprovider"

  remove_path "$target_home/Library/Containers/com.getdropbox.dropbox.TransferExtension"
  remove_path "$target_home/Library/Containers/com.getdropbox.dropbox.fileprovider"
  remove_path "$target_home/Library/Containers/com.getdropbox.dropbox.garcon"

  remove_path "$target_home/Library/Group Containers/G7HH3F8CAK.com.getdropbox.dropbox.sync"
  remove_path "$target_home/Library/Group Containers/com.dropbox.client.crashpad"

  remove_path "$target_home/Library/Application Scripts/G7HH3F8CAK.com.getdropbox.dropbox.sync"
  remove_path "$target_home/Library/Application Scripts/com.dropbox.client.crashpad"
  remove_path "$target_home/Library/Application Scripts/com.getdropbox.dropbox.TransferExtension"
  remove_path "$target_home/Library/Application Scripts/com.getdropbox.dropbox.fileprovider"
  remove_path "$target_home/Library/Application Scripts/com.getdropbox.dropbox.garcon"

  remove_path "$target_home/Library/LaunchAgents/com.dropbox.DropboxUpdater.wake.plist"
  remove_path "$target_home/Library/LaunchAgents/com.dropbox.dropboxmacupdate.agent.plist"
  remove_path "$target_home/Library/LaunchAgents/com.dropbox.dropboxmacupdate.xpcservice.plist"

  remove_path "$target_home/Library/Preferences/com.dropbox.DropboxMacUpdate.plist"
  remove_path "$target_home/Library/Preferences/com.getdropbox.dropbox.plist"

  remove_path "$target_home/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.getdropbox.dropbox.sfl4"

  remove_path /Library/PrivilegedHelperTools/com.getdropbox.dropbox.UpdaterPrivilegedHelper
  remove_path /Library/LaunchDaemons/com.dropbox.DropboxMacUpdate.agent.plist
  remove_path /Library/LaunchDaemons/com.dropbox.dropboxmacupdate.agent.plist
  remove_path /Library/LaunchDaemons/com.getdropbox.dropbox.UpdaterPrivilegedHelper.plist

  remove_glob /private/var/db/receipts/com.dropbox.* /private/var/db/receipts/com.getdropbox.*
}

remove_caches_and_temp() {
  remove_glob \
    "$target_home/Library/Caches/com.dropbox"* \
    "$target_home/Library/Caches/com.getdropbox"* \
    "$target_home/Library/Caches/Dropbox"* \
    "$target_home/Library/Caches/"*Dropbox* \
    "$target_home/Library/HTTPStorages/com.dropbox"* \
    "$target_home/Library/HTTPStorages/com.getdropbox"* \
    "$target_home/Library/WebKit/com.dropbox"* \
    "$target_home/Library/WebKit/com.getdropbox"* \
    "$target_home/Library/Cookies/com.dropbox"* \
    "$target_home/Library/Cookies/com.getdropbox"* \
    "$target_home/Library/Logs/Dropbox"* \
    "$target_home/Library/Logs/com.dropbox"* \
    "$target_home/Library/Logs/com.getdropbox"* \
    "$target_home/Library/Saved Application State/com.dropbox"* \
    "$target_home/Library/Saved Application State/com.getdropbox"*

  remove_glob \
    /private/var/folders/*/*/*/com.dropbox* \
    /private/var/folders/*/*/*/com.getdropbox* \
    /private/tmp/com.dropbox* \
    /private/tmp/com.getdropbox* \
    /tmp/com.dropbox* \
    /tmp/com.getdropbox* \
    /var/tmp/com.dropbox* \
    /var/tmp/com.getdropbox*
}

audit_path_is_dropbox_owned() {
  local path="$1"

  case "$path" in
  *Dropbox* | *dropbox* | *getdropbox* | *G7HH3F8CAK.com.getdropbox.dropbox.sync*)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

audit_path_is_safe_side_effect() {
  local path="$1"

  case "$path" in
  "$target_home/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.getdropbox.dropbox.sfl4")
    return 0
    ;;
  /Applications/.DS_Store)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

remove_from_audit_list() {
  if [ -z "$audit_list" ] || [ ! -f "$audit_list" ]; then
    return
  fi

  echo "Using audit list $audit_list"

  local reversed_list
  reversed_list="$(/usr/bin/mktemp "/tmp/dropbox-uninstall-audit.XXXXXX")"
  /usr/bin/tail -r "$audit_list" >"$reversed_list"

  local path
  while IFS= read -r path; do
    path="${path/#\/Users\/admin/$target_home}"

    if audit_path_is_dropbox_owned "$path"; then
      if [ "$remove_app_bundle" -eq 0 ]; then
        case "$path" in
        /Applications/Dropbox.app | /Applications/Dropbox.app/* | /System/Volumes/Data/Applications/Dropbox.app | /System/Volumes/Data/Applications/Dropbox.app/*)
          continue
          ;;
        esac
      fi

      if [ "$keep_synced_content" -eq 1 ]; then
        case "$path" in
        "$target_home/Dropbox" | "$target_home/Dropbox/"* | "$target_home/Library/CloudStorage/Dropbox" | "$target_home/Library/CloudStorage/Dropbox/"*)
          continue
          ;;
        esac
      fi
      remove_path "$path"
      continue
    fi

    if [ "$remove_audit_side_effects" -eq 1 ] && audit_path_is_safe_side_effect "$path"; then
      remove_path "$path"
    fi
  done <"$reversed_list"

  /bin/rm -f "$reversed_list"
}

prune_empty_dirs() {
  if [ "$keep_synced_content" -eq 0 ]; then
    rmdir_empty "$target_home/Library/CloudStorage/Dropbox"
  fi

  rmdir_empty "$target_home/.dropbox"
  rmdir_empty "$target_home/Library/Application Support/Dropbox"
  rmdir_empty "$target_home/Library/Dropbox"
  rmdir_empty "$target_home/Library/Application Support/FileProvider/com.getdropbox.dropbox.fileprovider"

  rmdir_empty "$target_home/Library/Application Scripts/G7HH3F8CAK.com.getdropbox.dropbox.sync"
  rmdir_empty "$target_home/Library/Application Scripts/com.dropbox.client.crashpad"
  rmdir_empty "$target_home/Library/Application Scripts/com.getdropbox.dropbox.TransferExtension"
  rmdir_empty "$target_home/Library/Application Scripts/com.getdropbox.dropbox.fileprovider"
  rmdir_empty "$target_home/Library/Application Scripts/com.getdropbox.dropbox.garcon"

  rmdir_empty "$target_home/Library/Group Containers/G7HH3F8CAK.com.getdropbox.dropbox.sync"
  rmdir_empty "$target_home/Library/Group Containers/com.dropbox.client.crashpad"

  rmdir_empty "$target_home/Library/Containers/com.getdropbox.dropbox.TransferExtension"
  rmdir_empty "$target_home/Library/Containers/com.getdropbox.dropbox.fileprovider"
  rmdir_empty "$target_home/Library/Containers/com.getdropbox.dropbox.garcon"
}

shopt -s nullglob

unload_launch_items
stop_dropbox
ensure_no_dropbox_processes
reset_shared_registrations
remove_core_paths
remove_caches_and_temp
remove_from_audit_list
prune_empty_dirs

echo "Dropbox uninstall cleanup finished"
if [ "$apply" -eq 0 ]; then
  echo "Dry run only. No files were deleted."
fi

What this fixed

I tested the script after reproducing the Dropbox File Provider problem. After the complete local cleanup, Dropbox behaved as I expected during setup again, and the File Provider option became available.

That was my original goal. But the same cleanup is also useful for anyone who wants to remove Dropbox as if it had never been installed.

The script was tested and verified against the Dropbox macOS release that was current when I wrote it. Dropbox can change its install layout in future releases, so the script may need updates later.

If you find that a newer Dropbox release leaves something behind, please report an issue in the GitHub repository.