Last active
September 23, 2026 16:46
-
-
Save heathdutton/12466b3fe4117eb75d186e1b53d6d66f to your computer and use it in GitHub Desktop.
Deskflow full reset to defaults (macOS) - run on both server and client - curl -sL xoop.install.id | bash
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Fixes the endless "Deskflow would like to control this computer" prompt without wiping config or reinstalling. | |
| # The logic lives in deskflow-reset.sh, this runs it in repair mode so the two can't drift apart. | |
| set -euo pipefail | |
| curl -fsSL https://gist.githubusercontent.com/heathdutton/12466b3fe4117eb75d186e1b53d6d66f/raw/deskflow-reset.sh \ | |
| | REPAIR=1 bash |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Deskflow nuke-and-pave (macOS). Run on both the server and the client. | |
| # | |
| # Swaps to the nightly cask (deskflow-dev) and wipes config back to defaults, so both machines end up on the same | |
| # build with fresh TLS certs. Then clears Deskflow's privacy permissions, walks through granting them once, adds a | |
| # login agent, adds a sleep hook on laptops, and starts it. | |
| # | |
| # Env vars go on the bash side of the pipe: curl -sL <url> | REPAIR=1 bash | |
| # CHANNEL=stable install the stable cask instead of the nightly one | |
| # REPAIR=1 skip the wipe and reinstall, fix permissions (the endless Accessibility prompt) and agents | |
| # AUTOSTART_ONLY=1 only set up the login agent and sleep hook, touch nothing else | |
| # SLEEP_KILL=1|0 force the quit-on-sleep hook on or off (default: on for Macs with a lid) | |
| set -euo pipefail | |
| CHANNEL="${CHANNEL:-dev}" | |
| REPAIR="${REPAIR:-0}" | |
| AUTOSTART_ONLY="${AUTOSTART_ONLY:-0}" | |
| SLEEP_KILL="${SLEEP_KILL:-auto}" | |
| case "$CHANNEL" in | |
| dev) CASK="deskflow-dev"; OTHER="deskflow" ;; | |
| stable) CASK="deskflow"; OTHER="deskflow-dev" ;; | |
| *) echo "CHANNEL must be 'dev' or 'stable', got '$CHANNEL'" >&2; exit 1 ;; | |
| esac | |
| [ "$(uname -s)" = "Darwin" ] || { echo "macOS only." >&2; exit 1; } | |
| APP="/Applications/Deskflow.app" | |
| BUNDLE_ID="org.deskflow.deskflow" | |
| LABEL="org.deskflow.autostart" | |
| PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" | |
| SLEEP_LABEL="org.deskflow.sleepkill" | |
| SLEEP_PLIST="$HOME/Library/LaunchAgents/$SLEEP_LABEL.plist" | |
| GUI="gui/$(id -u)" | |
| need_brew() { | |
| if ! command -v brew >/dev/null 2>&1; then | |
| for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do | |
| [ -x "$b" ] && eval "$("$b" shellenv)" && break | |
| done | |
| fi | |
| command -v brew >/dev/null 2>&1 || { echo "Homebrew required: https://brew.sh" >&2; exit 1; } | |
| } | |
| # Unload the agents first so neither can relaunch or kill Deskflow in the middle of anything below. | |
| launchctl bootout "$GUI/$LABEL" 2>/dev/null || true | |
| launchctl bootout "$GUI/$SLEEP_LABEL" 2>/dev/null || true | |
| if [ "$AUTOSTART_ONLY" != "1" ]; then | |
| echo "==> Quitting Deskflow" | |
| killall Deskflow deskflow-core deskflow-server 2>/dev/null || true | |
| sleep 1 | |
| fi | |
| if [ "$AUTOSTART_ONLY" != "1" ] && [ "$REPAIR" != "1" ]; then | |
| need_brew | |
| echo "==> Backing up config" | |
| # Holds the TLS keypair, trusted-server/client fingerprints, and screen layout. | |
| ts="$(date +%Y%m%d-%H%M%S)" | |
| if [ -d "$HOME/Library/Deskflow" ]; then | |
| mv "$HOME/Library/Deskflow" "$HOME/Deskflow-backup-$ts" | |
| echo " saved to ~/Deskflow-backup-$ts" | |
| else | |
| echo " nothing to back up" | |
| fi | |
| echo "==> Removing existing install" | |
| # Both casks lay down Deskflow.app, so the other channel has to go or the install collides. | |
| for c in "$CASK" "$OTHER"; do | |
| brew list --cask "$c" >/dev/null 2>&1 && brew uninstall --cask --force "$c" || true | |
| done | |
| # A hand-dragged copy isn't brew-managed and would still win the collision. | |
| [ -e "$APP" ] && rm -rf "$APP" || true | |
| echo "==> Installing $CASK" | |
| brew tap deskflow/tap >/dev/null 2>&1 || true | |
| brew install --cask "deskflow/tap/$CASK" | |
| echo "==> Clearing GUI prefs" | |
| # cfprefsd caches the plist in memory and rewrites it on quit if left running. | |
| defaults delete org.deskflow.deskflow 2>/dev/null || true | |
| rm -f "$HOME/Library/Preferences/org.deskflow.deskflow.plist" \ | |
| "$HOME/Library/Preferences/State/Deskflow.state" | |
| killall cfprefsd 2>/dev/null || true | |
| fi | |
| [ -d "$APP" ] || { echo "$APP not found." >&2; exit 1; } | |
| if [ "$AUTOSTART_ONLY" != "1" ]; then | |
| echo "==> Checking signature" | |
| # TCC ties a grant to the signature the app had when the box was ticked. A Developer ID build matches on Team ID, | |
| # so its grant survives upgrades. An ad-hoc build matches on CDHash, and one whose seal doesn't verify can never | |
| # match at all. Re-signing ad-hoc gives that build a valid seal. A build that already verifies is left alone, since | |
| # re-signing it would throw away its Developer ID signature and notarization. | |
| if codesign --verify --deep --strict "$APP" >/dev/null 2>&1; then | |
| team="$(codesign -dv "$APP" 2>&1 | sed -n 's/^TeamIdentifier=//p')" | |
| echo " seal intact (team: ${team:-none}), leaving it alone" | |
| else | |
| echo " seal broken, re-signing ad-hoc" | |
| codesign --force --deep --sign - "$APP" 2>&1 | sed 's/^/ /' | |
| if ! codesign --verify --deep --strict "$APP" >/dev/null 2>&1; then | |
| echo "Re-sign failed, seal still invalid." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| echo "==> Clearing Deskflow's privacy permissions" | |
| # A ticked row recorded against an earlier build's signature stays ticked in System Settings, but the current app | |
| # no longer matches it, so macOS asks forever. Clearing it makes the next grant record the signature that's | |
| # installed now. Accessibility = control the Mac, ListenEvent = Input Monitoring, PostEvent = send keystrokes. | |
| for svc in Accessibility ListenEvent PostEvent; do | |
| if tccutil reset "$svc" "$BUNDLE_ID" >/dev/null 2>&1; then echo " reset $svc"; else echo " skipped $svc"; fi | |
| done | |
| fi | |
| echo "==> Installing login agent" | |
| # Deskflow has no start-at-login setting, so launchd does it. Going through `open` makes TCC see the app bundle as the | |
| # requester. No KeepAlive: Deskflow exits with an error while Accessibility is off, and respawning it would re-pop the | |
| # permission prompt every few seconds. | |
| mkdir -p "$HOME/Library/LaunchAgents" | |
| cat > "$PLIST" <<PLIST_EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key> | |
| <string>$LABEL</string> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>/usr/bin/open</string> | |
| <string>-a</string> | |
| <string>$APP</string> | |
| </array> | |
| <key>RunAtLoad</key> | |
| <true/> | |
| <key>StandardOutPath</key> | |
| <string>$HOME/Library/Logs/deskflow-autostart.log</string> | |
| <key>StandardErrorPath</key> | |
| <string>$HOME/Library/Logs/deskflow-autostart.log</string> | |
| </dict> | |
| </plist> | |
| PLIST_EOF | |
| plutil -lint "$PLIST" >/dev/null || { echo "generated plist is invalid" >&2; exit 1; } | |
| if [ "$SLEEP_KILL" = "auto" ]; then | |
| # Only laptops carry the lid key. A desktop idling to sleep would take the client down with no keyboard left that | |
| # could start it again, since that keyboard is the one Deskflow was sharing. | |
| ioreg -r -k AppleClamshellState -d 1 | grep -q AppleClamshellState && SLEEP_KILL=1 || SLEEP_KILL=0 | |
| fi | |
| if [ "$SLEEP_KILL" = "1" ]; then | |
| echo "==> Installing sleep hook" | |
| # Quits Deskflow whenever the Mac sleeps, so a stuck instance never outlives a lid close and nothing drains the | |
| # battery. It stays off after wake until started by hand or at the next login. Clamshell mode with an external | |
| # display keeps the Mac awake, so closing the lid there quits nothing. | |
| need_brew | |
| # stdout hidden because its caveats describe ~/.sleep scripts, which this agent doesn't use. Errors still show. | |
| brew list sleepwatcher >/dev/null 2>&1 || brew install sleepwatcher >/dev/null | |
| sw="$(brew --prefix)/sbin/sleepwatcher" | |
| [ -x "$sw" ] || { echo "sleepwatcher missing at $sw" >&2; exit 1; } | |
| # TERM first so it can shut down cleanly, then KILL for when it's wedged and ignores that. sleepwatcher hands this | |
| # string to the shell, so the semicolons chain. | |
| kill_cmd="/usr/bin/killall -q Deskflow deskflow-core; /bin/sleep 2; /usr/bin/killall -q -9 Deskflow deskflow-core" | |
| cat > "$SLEEP_PLIST" <<PLIST_EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key> | |
| <string>$SLEEP_LABEL</string> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>$sw</string> | |
| <string>-V</string> | |
| <string>-s</string> | |
| <string>$kill_cmd</string> | |
| </array> | |
| <key>RunAtLoad</key> | |
| <true/> | |
| <key>KeepAlive</key> | |
| <true/> | |
| <key>StandardOutPath</key> | |
| <string>$HOME/Library/Logs/deskflow-sleepkill.log</string> | |
| <key>StandardErrorPath</key> | |
| <string>$HOME/Library/Logs/deskflow-sleepkill.log</string> | |
| </dict> | |
| </plist> | |
| PLIST_EOF | |
| plutil -lint "$SLEEP_PLIST" >/dev/null || { echo "generated sleep plist is invalid" >&2; exit 1; } | |
| launchctl bootstrap "$GUI" "$SLEEP_PLIST" | |
| echo " Deskflow quits when this Mac sleeps and stays off until you start it" | |
| else | |
| rm -f "$SLEEP_PLIST" | |
| echo "==> Skipping sleep hook (no lid, or SLEEP_KILL=0)" | |
| fi | |
| echo "==> Starting Deskflow" | |
| # RunAtLoad starts it now as well as at every login. | |
| launchctl bootstrap "$GUI" "$PLIST" | |
| sleep 2 | |
| if [ "$AUTOSTART_ONLY" != "1" ]; then | |
| echo "==> Granting permissions" | |
| echo " Switch Deskflow on in Privacy & Security > Accessibility, and in Input Monitoring if it asks." | |
| open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" 2>/dev/null || true | |
| # Under curl | bash the pipe is stdin, so the keypress has to come from the terminal. | |
| if (exec </dev/tty) 2>/dev/null; then | |
| printf ' Press Enter once it is on... ' | |
| read -r _ </dev/tty || true | |
| # Restart so it picks up the grant, since it may have already exited without one. | |
| killall Deskflow deskflow-core 2>/dev/null || true | |
| sleep 1 | |
| open -a "$APP" | |
| sleep 2 | |
| else | |
| echo " No terminal to wait on. Once it's on, restart it: killall Deskflow; open -a Deskflow" | |
| fi | |
| fi | |
| ver="$("$APP/Contents/MacOS/deskflow-core" --version 2>/dev/null | head -1)" | |
| echo | |
| echo "Done. Installed: ${ver:-unknown}" | |
| pgrep -qf "$APP/Contents/MacOS/Deskflow" \ | |
| && echo "Deskflow is running and starts at login." \ | |
| || echo "Deskflow isn't running. Start it with: open -a Deskflow" | |
| echo "Remove autostart with: launchctl bootout $GUI/$LABEL && rm $PLIST" | |
| if [ -f "$SLEEP_PLIST" ]; then | |
| echo "Remove the sleep hook with: launchctl bootout $GUI/$SLEEP_LABEL && rm $SLEEP_PLIST" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment