Skip to content

Instantly share code, notes, and snippets.

@sethwebster
Created January 30, 2026 20:09
Show Gist options
  • Select an option

  • Save sethwebster/af3a18440c5640d32de52887cafd9e5a to your computer and use it in GitHub Desktop.

Select an option

Save sethwebster/af3a18440c5640d32de52887cafd9e5a to your computer and use it in GitHub Desktop.
Clean up OpenClaw install
#!/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