Created
January 30, 2026 20:09
-
-
Save sethwebster/af3a18440c5640d32de52887cafd9e5a to your computer and use it in GitHub Desktop.
Clean up OpenClaw install
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
| #!/bin/bash | |
| set -euo pipefail | |
| # OpenClaw Uninstaller for macOS and Linux | |
| # Reverses everything done by the OpenClaw installer | |
| # Usage: bash uninstall-openclaw.sh [options] | |
| # | |
| # Security features: | |
| # - Path validation prevents directory traversal attacks | |
| # - Symlink handling avoids deleting unexpected targets | |
| # - OpenClaw-specific pattern matching preserves other tools' shell config | |
| # - Temp file cleanup on exit/interrupt | |
| BOLD='\033[1m' | |
| ERROR='\033[38;2;226;61;45m' | |
| WARN='\033[38;2;255;176;32m' | |
| SUCCESS='\033[38;2;47;191;113m' | |
| INFO='\033[38;2;255;138;91m' | |
| MUTED='\033[38;2;139;127;119m' | |
| NC='\033[0m' | |
| DRY_RUN=${OPENCLAW_UNINSTALL_DRY_RUN:-0} | |
| NO_PROMPT=${OPENCLAW_UNINSTALL_NO_PROMPT:-0} | |
| KEEP_CONFIG=${OPENCLAW_UNINSTALL_KEEP_CONFIG:-0} | |
| VERBOSE=${OPENCLAW_UNINSTALL_VERBOSE:-0} | |
| TEMP_FILES=() | |
| cleanup_temp_files() { | |
| for f in "${TEMP_FILES[@]:-}"; do | |
| rm -f "$f" 2>/dev/null || true | |
| done | |
| } | |
| trap cleanup_temp_files EXIT INT TERM | |
| print_usage() { | |
| cat <<EOF | |
| OpenClaw uninstaller (macOS + Linux) | |
| Removes: | |
| • npm global install (openclaw package) | |
| • git checkout (~/openclaw or custom directory) | |
| • config directory (~/.openclaw) | |
| • legacy configs (~/.clawdbot, ~/.moltbot, ~/.moldbot) | |
| • git wrapper (~/.local/bin/openclaw) | |
| • PATH modifications in shell configs | |
| • workspace directories | |
| Keeps (not removed): | |
| • Homebrew | |
| • Node.js | |
| • Git | |
| • pnpm | |
| • npm global config | |
| Usage: | |
| bash uninstall-openclaw.sh [options] | |
| Options: | |
| --dry-run Show what would be removed (no changes) | |
| --no-prompt Don't ask for confirmation (required for CI) | |
| --keep-config Keep config files (~/.openclaw) | |
| --verbose Print debug output | |
| --help, -h Show this help | |
| Environment variables: | |
| OPENCLAW_UNINSTALL_DRY_RUN=1 | |
| OPENCLAW_UNINSTALL_NO_PROMPT=1 | |
| OPENCLAW_UNINSTALL_KEEP_CONFIG=1 | |
| OPENCLAW_UNINSTALL_VERBOSE=1 | |
| Examples: | |
| bash uninstall-openclaw.sh | |
| bash uninstall-openclaw.sh --dry-run | |
| bash uninstall-openclaw.sh --keep-config --no-prompt | |
| EOF | |
| } | |
| parse_args() { | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --dry-run) DRY_RUN=1; shift ;; | |
| --no-prompt) NO_PROMPT=1; shift ;; | |
| --keep-config) KEEP_CONFIG=1; shift ;; | |
| --verbose) VERBOSE=1; shift ;; | |
| --help|-h) print_usage; exit 0 ;; | |
| *) shift ;; | |
| esac | |
| done | |
| } | |
| configure_verbose() { | |
| if [[ "$VERBOSE" == "1" ]]; then | |
| set -x | |
| fi | |
| } | |
| is_promptable() { | |
| [[ "$NO_PROMPT" != "1" && -r /dev/tty && -w /dev/tty ]] | |
| } | |
| prompt_yn() { | |
| local prompt="$1" | |
| local default="${2:-n}" | |
| if ! is_promptable; then | |
| [[ "$default" == "y" ]] | |
| return | |
| fi | |
| local answer="" | |
| echo -e "${prompt} [y/N]: " > /dev/tty | |
| read -r answer < /dev/tty || true | |
| answer="$(echo "$answer" | tr '[:upper:]' '[:lower:]')" | |
| [[ "$answer" == "y" || "$answer" == "yes" ]] | |
| } | |
| safe_remove() { | |
| local path="$1" | |
| local desc="$2" | |
| if [[ ! -e "$path" && ! -L "$path" ]]; then | |
| return 0 | |
| fi | |
| # Check if it's a symlink pointing outside expected directories | |
| if [[ -L "$path" ]]; then | |
| local target="" | |
| target="$(readlink "$path" 2>/dev/null || true)" | |
| if [[ -n "$target" ]]; then | |
| echo -e "${WARN}→${NC} Found symlink: ${INFO}${path}${NC} → ${target}" | |
| case "$target" in | |
| "$HOME"/*|/tmp/*|/var/tmp/*) | |
| echo -e " ${MUTED}${desc}${NC}" | |
| ;; | |
| *) | |
| echo -e " ${ERROR}Warning: symlink target outside HOME${NC}" | |
| echo -e " ${MUTED}Will only remove symlink, not target${NC}" | |
| ;; | |
| esac | |
| fi | |
| else | |
| echo -e "${WARN}→${NC} Found: ${INFO}${path}${NC}" | |
| echo -e " ${MUTED}${desc}${NC}" | |
| fi | |
| if [[ "$DRY_RUN" == "1" ]]; then | |
| echo -e "${MUTED} [dry-run] Would remove${NC}" | |
| return 0 | |
| fi | |
| # For symlinks, just remove the link, not the target | |
| if [[ -L "$path" ]]; then | |
| if rm "$path" 2>/dev/null; then | |
| echo -e "${SUCCESS}✓${NC} Removed symlink" | |
| else | |
| echo -e "${ERROR}✗${NC} Failed to remove symlink: ${path}" | |
| return 1 | |
| fi | |
| else | |
| if rm -rf "$path" 2>/dev/null; then | |
| echo -e "${SUCCESS}✓${NC} Removed" | |
| else | |
| echo -e "${ERROR}✗${NC} Failed to remove: ${path}" | |
| return 1 | |
| fi | |
| fi | |
| } | |
| remove_line_from_file() { | |
| local file="$1" | |
| local pattern="$2" | |
| local desc="$3" | |
| if [[ ! -f "$file" ]]; then | |
| return 0 | |
| fi | |
| if ! grep -q "$pattern" "$file" 2>/dev/null; then | |
| return 0 | |
| fi | |
| echo -e "${WARN}→${NC} Found in: ${INFO}${file}${NC}" | |
| echo -e " ${MUTED}${desc}${NC}" | |
| if [[ "$DRY_RUN" == "1" ]]; then | |
| echo -e "${MUTED} [dry-run] Would remove lines matching: ${pattern}${NC}" | |
| return 0 | |
| fi | |
| local tmp="${file}.tmp.$$" | |
| TEMP_FILES+=("$tmp") | |
| if grep -v "$pattern" "$file" > "$tmp" && mv "$tmp" "$file"; then | |
| echo -e "${SUCCESS}✓${NC} Cleaned" | |
| return 0 | |
| else | |
| echo -e "${ERROR}✗${NC} Failed to clean: ${file}" | |
| return 1 | |
| fi | |
| } | |
| stop_daemon() { | |
| if ! command -v openclaw &>/dev/null; then | |
| return 0 | |
| fi | |
| echo -e "${WARN}→${NC} Checking for running daemon..." | |
| if [[ "$DRY_RUN" == "1" ]]; then | |
| echo -e "${MUTED} [dry-run] Would stop daemon${NC}" | |
| return 0 | |
| fi | |
| if openclaw daemon stop 2>/dev/null; then | |
| echo -e "${SUCCESS}✓${NC} Daemon stopped" | |
| else | |
| echo -e "${MUTED} No daemon running${NC}" | |
| fi | |
| } | |
| remove_service_definitions() { | |
| # macOS launchd | |
| local launchd_plist="$HOME/Library/LaunchAgents/com.openclaw.daemon.plist" | |
| if [[ -f "$launchd_plist" ]]; then | |
| echo -e "${WARN}→${NC} Found launchd service definition" | |
| if [[ "$DRY_RUN" != "1" ]]; then | |
| launchctl unload "$launchd_plist" 2>/dev/null || true | |
| fi | |
| safe_remove "$launchd_plist" "launchd service definition" | |
| fi | |
| # Linux systemd (user service) | |
| local systemd_service="$HOME/.config/systemd/user/openclaw.service" | |
| if [[ -f "$systemd_service" ]]; then | |
| echo -e "${WARN}→${NC} Found systemd service definition" | |
| if [[ "$DRY_RUN" != "1" ]]; then | |
| systemctl --user stop openclaw.service 2>/dev/null || true | |
| systemctl --user disable openclaw.service 2>/dev/null || true | |
| fi | |
| safe_remove "$systemd_service" "systemd service definition" | |
| if [[ "$DRY_RUN" != "1" ]]; then | |
| systemctl --user daemon-reload 2>/dev/null || true | |
| fi | |
| fi | |
| } | |
| uninstall_npm_global() { | |
| if ! npm list -g openclaw &>/dev/null; then | |
| return 0 | |
| fi | |
| echo -e "${WARN}→${NC} Found npm global install" | |
| if [[ "$DRY_RUN" == "1" ]]; then | |
| echo -e "${MUTED} [dry-run] Would run: npm uninstall -g openclaw${NC}" | |
| return 0 | |
| fi | |
| if npm uninstall -g openclaw 2>/dev/null; then | |
| echo -e "${SUCCESS}✓${NC} Removed npm global install" | |
| else | |
| echo -e "${ERROR}✗${NC} Failed to uninstall npm package" | |
| return 1 | |
| fi | |
| } | |
| validate_git_dir() { | |
| local dir="$1" | |
| # Canonicalize path | |
| if [[ ! -d "$dir" ]]; then | |
| return 1 | |
| fi | |
| local canonical="" | |
| canonical="$(cd "$dir" && pwd -P 2>/dev/null || true)" | |
| if [[ -z "$canonical" ]]; then | |
| return 1 | |
| fi | |
| # Must be under $HOME (prevent path traversal attacks) | |
| case "$canonical" in | |
| "$HOME"/*|"$HOME") echo "$canonical"; return 0 ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| find_git_checkouts() { | |
| local candidates=( | |
| "$HOME/openclaw" | |
| "$HOME/.openclaw-src" | |
| ) | |
| # Validate OPENCLAW_GIT_DIR if set | |
| if [[ -n "${OPENCLAW_GIT_DIR:-}" ]]; then | |
| local validated="" | |
| validated="$(validate_git_dir "${OPENCLAW_GIT_DIR}" || true)" | |
| if [[ -n "$validated" ]]; then | |
| candidates+=("$validated") | |
| else | |
| echo -e "${WARN}→${NC} Ignoring invalid OPENCLAW_GIT_DIR: ${OPENCLAW_GIT_DIR}" >&2 | |
| fi | |
| fi | |
| for dir in "${candidates[@]}"; do | |
| if [[ -z "$dir" || ! -d "$dir/.git" || ! -f "$dir/package.json" ]]; then | |
| continue | |
| fi | |
| if grep -q '"name"[[:space:]]*:[[:space:]]*"openclaw"' "$dir/package.json" 2>/dev/null; then | |
| echo "$dir" | |
| fi | |
| done | |
| } | |
| main() { | |
| echo -e "${BOLD}🦞 OpenClaw Uninstaller${NC}" | |
| echo "" | |
| if [[ "$DRY_RUN" == "1" ]]; then | |
| echo -e "${INFO}i${NC} Dry run mode - no changes will be made" | |
| echo "" | |
| fi | |
| if [[ "$NO_PROMPT" != "1" ]]; then | |
| echo -e "${WARN}This will remove OpenClaw from your system.${NC}" | |
| if [[ "$KEEP_CONFIG" != "1" ]]; then | |
| echo -e "${WARN}Your config and workspace will be deleted.${NC}" | |
| fi | |
| echo "" | |
| if ! prompt_yn "Continue?" "n"; then | |
| echo -e "${MUTED}Cancelled.${NC}" | |
| exit 0 | |
| fi | |
| echo "" | |
| fi | |
| # Stop daemon first | |
| echo -e "${BOLD}Stopping services...${NC}" | |
| stop_daemon | |
| remove_service_definitions | |
| echo "" | |
| # Remove npm global install | |
| echo -e "${BOLD}Removing npm installation...${NC}" | |
| uninstall_npm_global | |
| echo "" | |
| # Remove git wrapper | |
| echo -e "${BOLD}Removing git wrapper...${NC}" | |
| safe_remove "$HOME/.local/bin/openclaw" "Git checkout wrapper script" | |
| echo "" | |
| # Remove git checkouts | |
| echo -e "${BOLD}Removing git checkouts...${NC}" | |
| local found_checkout=0 | |
| while IFS= read -r dir; do | |
| safe_remove "$dir" "Git source checkout" | |
| found_checkout=1 | |
| done < <(find_git_checkouts) | |
| if [[ "$found_checkout" == "0" ]]; then | |
| echo -e "${MUTED}No git checkouts found${NC}" | |
| fi | |
| echo "" | |
| # Remove npm stale files | |
| echo -e "${BOLD}Cleaning npm global cache...${NC}" | |
| local npm_root="" | |
| npm_root="$(npm root -g 2>/dev/null || true)" | |
| if [[ -n "$npm_root" && -d "$npm_root" ]]; then | |
| local found_npm_files=0 | |
| # Remove .openclaw-* files (enumerate, don't glob directly) | |
| while IFS= read -r -d '' file; do | |
| safe_remove "$file" "npm cache file" | |
| found_npm_files=1 | |
| done < <(find "$npm_root" -maxdepth 1 -name '.openclaw-*' -print0 2>/dev/null || true) | |
| # Remove openclaw package directory | |
| if [[ -e "$npm_root/openclaw" || -L "$npm_root/openclaw" ]]; then | |
| safe_remove "$npm_root/openclaw" "npm package" | |
| found_npm_files=1 | |
| fi | |
| if [[ "$found_npm_files" == "0" ]]; then | |
| echo -e "${MUTED}No npm cache files found${NC}" | |
| fi | |
| else | |
| echo -e "${MUTED}npm global directory not found${NC}" | |
| fi | |
| echo "" | |
| # Remove config and data | |
| if [[ "$KEEP_CONFIG" != "1" ]]; then | |
| echo -e "${BOLD}Removing configuration and data...${NC}" | |
| safe_remove "$HOME/.openclaw" "Config, workspace, and data" | |
| safe_remove "$HOME/.clawdbot" "Legacy config (clawdbot)" | |
| safe_remove "$HOME/.moltbot" "Legacy config (moltbot)" | |
| safe_remove "$HOME/.moldbot" "Legacy config (moldbot)" | |
| echo "" | |
| else | |
| echo -e "${INFO}i${NC} Keeping config directory: ${INFO}$HOME/.openclaw${NC}" | |
| echo "" | |
| fi | |
| # Clean shell config files (only OpenClaw-specific lines) | |
| echo -e "${BOLD}Cleaning shell configuration files...${NC}" | |
| local cleaned_any=0 | |
| for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.bash_profile" "$HOME/.profile"; do | |
| # Look for lines with both npm-global AND openclaw context | |
| if remove_line_from_file "$rc" 'npm-global.*openclaw\|openclaw.*npm-global' "OpenClaw npm PATH"; then | |
| cleaned_any=1 | |
| fi | |
| # Look for lines with both .local/bin AND openclaw context | |
| if remove_line_from_file "$rc" '\.local/bin.*openclaw\|openclaw.*\.local/bin' "OpenClaw local bin PATH"; then | |
| cleaned_any=1 | |
| fi | |
| # Look for generic installer comments | |
| if remove_line_from_file "$rc" '# Added by OpenClaw installer' "OpenClaw installer comment"; then | |
| cleaned_any=1 | |
| fi | |
| done | |
| if [[ "$cleaned_any" == "0" ]]; then | |
| echo -e "${MUTED}No OpenClaw PATH modifications found in shell configs${NC}" | |
| echo -e "${MUTED}(Generic .npm-global/.local/bin entries preserved)${NC}" | |
| fi | |
| echo "" | |
| # Summary | |
| echo -e "${SUCCESS}${BOLD}✓ Uninstall complete${NC}" | |
| echo "" | |
| # Note about system dependencies | |
| local deps=() | |
| [[ -n "$(command -v node 2>/dev/null || true)" ]] && deps+=("Node.js $(node -v)") | |
| [[ -n "$(command -v npm 2>/dev/null || true)" ]] && deps+=("npm $(npm -v)") | |
| [[ -n "$(command -v pnpm 2>/dev/null || true)" ]] && deps+=("pnpm $(pnpm -v)") | |
| [[ -n "$(command -v git 2>/dev/null || true)" ]] && deps+=("git $(git --version | cut -d' ' -f3)") | |
| [[ -n "$(command -v brew 2>/dev/null || true)" ]] && deps+=("Homebrew $(brew --version | head -n1 | cut -d' ' -f2)") | |
| if [[ "${#deps[@]}" -gt 0 ]]; then | |
| echo -e "${MUTED}System dependencies (not removed):${NC}" | |
| for dep in "${deps[@]}"; do | |
| echo -e " ${MUTED}•${NC} ${dep}" | |
| done | |
| echo "" | |
| echo -e "${MUTED}To remove these, use your package manager:${NC}" | |
| echo -e " ${MUTED}macOS:${NC} brew uninstall node" | |
| echo -e " ${MUTED}Linux:${NC} sudo apt remove nodejs (or dnf/yum)" | |
| echo "" | |
| fi | |
| if [[ "$KEEP_CONFIG" == "1" ]]; then | |
| echo -e "${INFO}i${NC} Config preserved at: ${INFO}~/.openclaw${NC}" | |
| echo -e " To remove later: ${INFO}rm -rf ~/.openclaw${NC}" | |
| echo "" | |
| fi | |
| echo -e "${SUCCESS}OpenClaw has been removed from your system.${NC}" | |
| } | |
| parse_args "$@" | |
| configure_verbose | |
| main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment