#!/bin/bash # omarchy:summary=Remove the Hermes desktop app along with the Hermes runtime it installed. # omarchy:requires-sudo=true # -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /. set -euo pipefail ensure_hermes_stopped() { python3 - "$HOME" <<'PY' import os from pathlib import Path import sys home = Path(sys.argv[1]) roots = [str((home / relative).resolve()) for relative in ('.hermes', '.config/Hermes')] roots.append('/opt/hermes-desktop') def belongs_to_hermes(target): target = target.removesuffix(' (deleted)') return any(target == root or target.startswith(root + '/') for root in roots) holders = [] for process in Path('/proc').iterdir(): if not process.name.isdigit() or int(process.name) == os.getpid(): continue try: if process.stat().st_uid != os.getuid(): continue targets = [os.fsdecode(arg) for arg in (process / 'cmdline').read_bytes().split(b'\0')] entries = [process / 'exe', process / 'cwd'] try: entries.extend((process / 'fd').iterdir()) except PermissionError: pass for entry in entries: try: targets.append(os.readlink(entry)) except OSError: pass if any(belongs_to_hermes(target) for target in targets): holders.append(process.name) except (FileNotFoundError, ProcessLookupError, PermissionError): continue if holders: print('Close Hermes and processes using its files before removing it (PIDs: ' + ', '.join(holders) + '). Then try again.', file=sys.stderr) sys.exit(1) PY } # Removing an open SQLite WAL leaves a live writer on a deleted generation. ensure_hermes_stopped omarchy-pkg-drop hermes-desktop # The installer leaves a unit waiting to hand the app the Omarchy theme. systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true # The mise CLI is the app's predecessor, not the app itself: Hermes Desktop takes # it over on install and runs its own runtime instead, so a copy still here is one # the app never superseded -- an interrupted install, or the terminal CLI from # before the app existed. Remove Hermes clears that too, scoped by the installer # to what Omarchy owns so a hermes the user set up themselves is left alone. # Tolerated here rather than fatal, so the ~/.hermes handling below still runs; # the failure is answered for at the end instead of being swallowed. cli_removed=true ensure_hermes_stopped omarchy-install-hermes-cli --remove || cli_removed=false # The app writes this when the runtime it provisions under ~/.hermes has landed, # and it is the only thing that tells that runtime apart from one the user # installed themselves -- the paths are the same either way. Without it the app # never got that far: a machine where it was installed but never launched still # has whatever was there before, and none of it is ours to delete unasked. if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then # The checkout and venv, its own uv, its own node. None of it is any use once # the app is gone, so it goes without asking; what the user made with the app # is a different question, answered below. rm -rf \ "$HOME/.hermes/hermes-agent" \ "$HOME/.hermes/bootstrap-cache" \ "$HOME/.hermes/bin" \ "$HOME/.hermes/node" # Only the wrappers pointing into ~/.hermes, matched as a plain string: the # path carries a dot, so an unanchored pattern would also claim a wrapper # pointing at a sibling like ~/xhermes. for command in hermes hermes-agent hermes-acp; do wrapper="$HOME/.local/bin/$command" if [[ -f $wrapper && ! -L $wrapper ]] && grep -qF "$HOME/.hermes" "$wrapper"; then rm -f "$wrapper" fi done # When Hermes brought its own Node it symlinked these next to its own commands, # and they point at what we just deleted. Only the links into ~/.hermes: a # system Node, or someone else's, lives somewhere else entirely. for command in node npm npx; do link="$HOME/.local/bin/$command" if [[ -L $link && $(readlink "$link") == "$HOME/.hermes"/* ]]; then rm -f "$link" fi done fi # What survives to here is the user's: the chats, memories and skills in # ~/.hermes, the connections and their encrypted tokens in ~/.config/Hermes. # Keeping them stays the default -- they are small, and finding them intact # after a reinstall is the better surprise -- but a removal meant to be # complete should not leave credentials behind either, so the choice is put in # front of the user with the size, default no. Asked whenever the directories # exist, marker or no marker: on a machine where the marker never appeared the # data came from the terminal CLI or an install the app never finished, and it # is still what removal is asked to clean up. Naming the paths keeps the # question honest there too -- ~/.hermes may still carry a runtime the app # never owned, a yes takes that with it, and saying so is the prompt's job. # Without a terminal to ask in, keeping everything is the answer. data_removed=false if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]] && omarchy-cmd-present gum; then # du answers non-zero when either directory is missing, and pipefail would # turn that into an aborted removal; the size is worth no such thing. size=$(du -shc "$HOME/.hermes" "$HOME/.config/Hermes" 2>/dev/null | tail -1 | cut -f1 || true) if gum confirm --default=false "Also delete ~/.hermes and ~/.config/Hermes ($size: chats, memories, skills, connections and tokens)?"; then ensure_hermes_stopped rm -rf "$HOME/.hermes" "$HOME/.config/Hermes" data_removed=true fi fi echo "" echo "Hermes Desktop has been removed." if [[ $data_removed == true ]]; then echo "Its chats, memories, and settings in ~/.hermes and ~/.config/Hermes are gone too." elif [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]]; then echo "Your chats, memories, and skills are still in ~/.hermes," echo "and your connections and settings in ~/.config/Hermes." fi # The messages above still hold -- the app and its runtime are gone -- but a CLI # teardown that failed already said so on stderr, and that stands. if [[ $cli_removed == "false" ]]; then exit 1 fi