#!/usr/bin/env bash # # AgentKey uninstaller for macOS and Linux # Usage: curl -fsSL https://agentkey.app/uninstall.sh | bash # curl -fsSL https://agentkey.app/uninstall.sh | bash -s -- --keep-marketplace # curl -fsSL https://agentkey.app/uninstall.sh | bash -s -- --force-in-repo # # Cleans up everything install.sh (and the legacy two-command flow) ever wrote: # 1. Skill files in every agent (via `skills remove` — fans across 40+ agents) # 2. MCP server entries (Claude Code / Claude Desktop / Cursor configs) # 3. Claude Code plugin + marketplace registrations (legacy plugin install path) # 4. Plugin / marketplace / npx caches # 5. Shell RC exports + CLAUDE.md sections (legacy) # 6. MCP stdio log set -euo pipefail KEEP_MARKETPLACE=false FORCE_IN_REPO=false SKIP_SKILL_REMOVE=false for arg in "$@"; do case "$arg" in --keep-marketplace) KEEP_MARKETPLACE=true ;; --force-in-repo) FORCE_IN_REPO=true ;; --skip-skill-remove) SKIP_SKILL_REMOVE=true ;; -h|--help) cat </dev/null \ && ! $FORCE_IN_REPO; then printf "\n ${BOLD}AgentKey — Uninstall${NC}\n\n" printf " ${ERROR}Refusing to run inside the AgentKey-Skill source repo.${NC}\n" printf " Running here would wipe this repo's own .mcp.json and CLAUDE.md.\n" printf " Re-run with ${BOLD}--force-in-repo${NC} if you really mean it.\n\n" exit 2 fi printf "\n ${BOLD}AgentKey — Uninstall${NC}\n" printf " ${MUTED}https://agentkey.app${NC}\n" # ── 1. Remove the skill via skills CLI ──────────────────────────────────── step "1. Skill files" if $SKIP_SKILL_REMOVE; then skipped "Skipped (--skip-skill-remove)" elif ! command -v npx >/dev/null 2>&1; then warn "npx not found — skipping 'skills remove' (manual: npx skills remove agentkey -g)" else # `skills remove` takes the **skill name** (`agentkey`), not the repo path. # The CLI also exits 0 when nothing matches, so we inspect stdout instead. info "Running: npx -y skills remove agentkey -g -y" REMOVE_OUTPUT="$(npx -y skills remove agentkey -g -y 2>&1 || true)" if printf '%s\n' "$REMOVE_OUTPUT" | grep -q "Successfully removed"; then ok "Skill removed from detected agents" elif printf '%s\n' "$REMOVE_OUTPUT" | grep -q "No matching skills found"; then skipped "Not registered with 'skills' CLI (already removed or installed via plugin marketplace)" else warn "'skills remove' produced unexpected output — some agents may still have skill files" warn "Check manually: npx skills list -g" fi fi # ── 2. MCP config cleanup ──────────────────────────────────────────────── step "2. MCP server entries" OS="$(uname -s)" # All known JSON MCP config paths across the 16 auto-supported agents. The # scrub logic is intentionally schema-agnostic — it walks the JSON tree and # drops any key named "agentkey" / "agentkey.app AgentKey" wherever it # finds it, so the same scrubber handles: # - mcpServers.agentkey (Claude Code, Desktop, Cursor, Gemini, Windsurf, Warp, Qwen, iFlow, Kimi, Kiro) # - projects.*.mcpServers (Claude Code's per-project shape) # - mcp.agentkey (OpenCode, Crush) # - amp.mcpServers.agentkey (Amp's flat dotted key) # Keep this list in sync with AGENT_REGISTRY in # AgentKey-Server/cli/src/lib/mcp-clients.ts. MCP_JSON_CONFIGS=( "$HOME/.claude.json" # Claude Code "$HOME/.cursor/mcp.json" # Cursor "$HOME/.gemini/settings.json" # Gemini CLI "$HOME/.qwen/settings.json" # Qwen Code "$HOME/.iflow/settings.json" # iFlow CLI "$HOME/.kimi/mcp.json" # Kimi CLI "$HOME/.kiro/settings/mcp.json" # Kiro CLI "$HOME/.codeium/windsurf/mcp_config.json" # Windsurf "$HOME/.warp/.mcp.json" # Warp "$HOME/.config/opencode/opencode.json" # OpenCode (mcp.) "$HOME/.config/amp/settings.json" # Amp (amp.mcpServers.) "$HOME/.config/crush/crush.json" # Crush (mcp.) ) if [ "$OS" = "Darwin" ]; then MCP_JSON_CONFIGS+=("$HOME/Library/Application Support/Claude/claude_desktop_config.json") else MCP_JSON_CONFIGS+=("$HOME/.config/Claude/claude_desktop_config.json") fi # TOML configs — handled separately because we can't parse TOML without a # library. We splice out `[mcp_servers.agentkey]` blocks via line-scan. MCP_TOML_CONFIGS=( "$HOME/.codex/config.toml" # Codex CLI ) have_python() { command -v python3 >/dev/null 2>&1 || command -v python >/dev/null 2>&1; } py() { if command -v python3 >/dev/null 2>&1; then python3 "$@"; else python "$@"; fi; } if ! have_python; then warn "python not found — skipping JSON cleanup; edit these files manually:" for f in "${MCP_JSON_CONFIGS[@]}"; do [ -f "$f" ] && echo " $f"; done else for cfg in "${MCP_JSON_CONFIGS[@]}"; do if [ ! -f "$cfg" ]; then skipped "$(basename "$cfg") not found" continue fi RESULT=$(py - "$cfg" <<'EOF' import json, sys path = sys.argv[1] try: with open(path) as f: d = json.load(f) except Exception as e: print(f"ERROR: {e}"); sys.exit(0) # Schema-agnostic recursive scrub: drop any dict key whose name exactly # matches one of our known MCP server names (current + legacy). Covers every # dialect we write: mcpServers.agentkey, mcp.agentkey, # amp.mcpServers.agentkey, projects.X.mcpServers.agentkey, etc. # Exact-match (not substring) so we don't nuke unrelated user keys like # "agentkey-helper" or "my-agentkey-config". NAMES = {'agentkey', 'agentkey.app agentkey'} def scrub(obj): removed = 0 if isinstance(obj, dict): for k in list(obj.keys()): if k.lower() in NAMES: del obj[k]; removed += 1 else: removed += scrub(obj[k]) elif isinstance(obj, list): for item in obj: removed += scrub(item) return removed removed = scrub(d) if removed: with open(path, 'w') as f: json.dump(d, f, indent=2) print(removed) EOF ) if [ "$RESULT" = "0" ]; then skipped "No agentkey entry in $cfg" elif [[ "$RESULT" =~ ^[0-9]+$ ]]; then ok "Removed $RESULT entry/entries from $cfg" else warn "Failed to update $cfg: $RESULT" fi done fi # Codex TOML: splice out [mcp_servers.agentkey] blocks (bare + legacy quoted). # Bash-only — no Python dependency — because we never parse, only line-edit. for cfg in "${MCP_TOML_CONFIGS[@]}"; do if [ ! -f "$cfg" ]; then skipped "$(basename "$cfg") not found" continue fi if ! grep -qE '^\s*\[\s*mcp_servers\s*\.\s*(agentkey|"agentkey\.app AgentKey")\s*\]' "$cfg" 2>/dev/null; then skipped "No agentkey block in $cfg" continue fi # Line-scan: drop from our header line to the next [section] header or EOF. awk ' BEGIN { skip = 0 } /^[[:space:]]*\[[[:space:]]*mcp_servers[[:space:]]*\.[[:space:]]*(agentkey|"agentkey\.app AgentKey")[[:space:]]*\][[:space:]]*$/ { skip = 1; next } /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { skip = 0 } !skip { print } ' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg" ok "Removed agentkey block from $cfg" done # DSH cleanup is marker-scoped so unrelated home/profile patches survive. DSH_ROOT="${DSH_HOME:-}"; [[ -n "$DSH_ROOT" && "$DSH_ROOT" == *[![:space:]]* ]] || DSH_ROOT="$HOME/.dsh" case "$DSH_ROOT" in "~"|"~/"*) DSH_ROOT="$HOME${DSH_ROOT#"~"}" ;; /*) ;; *) DSH_ROOT="$PWD/$DSH_ROOT" ;; esac DSH_CLEANED=false for cfg in "$DSH_ROOT/cordis.patch.yml" "$DSH_ROOT"/profiles/*/cordis.patch.yml; do [ -f "$cfg" ] || continue; grep -qE '^# agentkey:start([[:space:]]|$)' "$cfg" 2>/dev/null || { skipped "No AgentKey DSH block in $cfg"; continue; }; tmp="${cfg}.agentkey-uninstall.$$" if awk 'BEGIN{s=0;i=0} /^# agentkey:start([[:space:]]|$)/{if(s)i=1;s=1;next} /^# agentkey:end([[:space:]]|$)/{if(!s)i=1;else{s=0;next}} !s{print} END{if(s||i)exit 42}' "$cfg" > "$tmp"; then grep -q '^[[:space:]]*[^#[:space:]]' "$tmp" || printf '\n[]\n' >> "$tmp" chmod 600 "$tmp"; mv "$tmp" "$cfg"; DSH_CLEANED=true; ok "Removed AgentKey DSH block from $cfg" else rm -f "$tmp"; warn "Malformed AgentKey markers in $cfg — left unchanged" fi done LEGACY_DSH_PRESET="$DSH_ROOT/.agent-presets/agentkey" if [ -e "$LEGACY_DSH_PRESET" ] || [ -L "$LEGACY_DSH_PRESET" ]; then DSH_PRESET_BACKUP="${LEGACY_DSH_PRESET}.backup-$(date -u +%Y%m%dT%H%M%SZ)"; while [ -e "$DSH_PRESET_BACKUP" ]; do DSH_PRESET_BACKUP="${DSH_PRESET_BACKUP}-1"; done; mv "$LEGACY_DSH_PRESET" "$DSH_PRESET_BACKUP"; ok "Archived legacy DSH preset at $DSH_PRESET_BACKUP"; fi DSH_SETTINGS="$DSH_ROOT/settings.yaml" if [ -f "$DSH_SETTINGS" ]; then tmp="${DSH_SETTINGS}.agentkey-uninstall.$$" if awk 'BEGIN{p=0;r=0} /^[[:space:]]*agent-presets\.default:[[:space:]]*["\047]?agentkey["\047]?[[:space:]]*(#.*)?$/{r=1;next} /^[^[:space:]#][^:]*:[[:space:]]*/{p=($0~/^agent-presets:[[:space:]]*(#.*)?$/);print;next} p&&/^[[:space:]]+default:[[:space:]]*["\047]?agentkey["\047]?[[:space:]]*(#.*)?$/{r=1;next} {print} END{if(r)exit 10}' "$DSH_SETTINGS" > "$tmp"; then status=0; else status=$?; fi case "$status" in 0) rm -f "$tmp" ;; 10) mv "$tmp" "$DSH_SETTINGS"; ok "Removed legacy agent-presets default from $DSH_SETTINGS" ;; *) rm -f "$tmp"; warn "Could not clean legacy DSH settings in $DSH_SETTINGS" ;; esac fi # ── 2b. CLI-registered agents (droid / openclaw) ───────────────────────── # These two agents have no documented file-edit path; we registered them via # their own CLIs (`droid mcp add`, `openclaw mcp set`), so we have to use the # CLI to unregister. Best-effort — silently skip if the CLI isn't on PATH or # the entry was never created. step "2b. CLI-registered agents (droid / openclaw)" if command -v droid >/dev/null 2>&1; then if droid mcp remove agentkey >/dev/null 2>&1; then ok "Removed agentkey from droid (\`droid mcp remove\`)" else skipped "No agentkey entry in droid (or already removed)" fi else skipped "droid CLI not on PATH" fi if command -v openclaw >/dev/null 2>&1; then if openclaw mcp unset agentkey >/dev/null 2>&1; then ok "Removed agentkey from openclaw (\`openclaw mcp unset\`)" else skipped "No agentkey entry in openclaw (or already removed)" fi else skipped "openclaw CLI not on PATH" fi # ── 3. Claude Code plugin registrations (legacy) ───────────────────────── step "3. Claude Code plugin registrations (legacy)" if ! command -v claude >/dev/null 2>&1; then skipped "Claude Code CLI not on PATH — nothing to do here" else PLUGIN_LIST=$(claude plugin list 2>/dev/null || true) MARKETS=$(echo "$PLUGIN_LIST" | grep -oE 'agentkey@[a-zA-Z0-9_-]+' || true) if [ -z "$MARKETS" ]; then skipped "No agentkey plugin registered" else while IFS= read -r entry; do name="${entry%%@*}" info "Uninstalling $entry ..." if claude plugin uninstall "$name" --scope user 2>/dev/null; then ok "Removed $entry" else warn "Could not remove $entry — try: claude plugin uninstall $name --scope user" fi done <<< "$MARKETS" fi # Legacy MCP registration via `claude mcp` if claude mcp list 2>/dev/null | grep -q "^agentkey"; then info "Removing MCP server 'agentkey' via claude CLI ..." if claude mcp remove agentkey 2>/dev/null; then ok "MCP server removed" else warn "Could not remove MCP server via claude CLI" fi else skipped "No 'agentkey' MCP via claude CLI" fi # Marketplace entry if $KEEP_MARKETPLACE; then skipped "Marketplace removal skipped (--keep-marketplace)" else AGENTKEY_MARKETS=$(claude plugin marketplace list 2>/dev/null \ | grep -B1 -A1 -E "(AgentKey-Skill|chainbase-labs/AgentKey-Skill|agentkey-skill|chainbase-labs/agentkey)" \ | grep -oE '^ ❯ [a-zA-Z0-9_-]+' | awk '{print $2}' || true) if [ -z "$AGENTKEY_MARKETS" ]; then skipped "No AgentKey marketplace entry" else while IFS= read -r mkt; do info "Removing marketplace '$mkt' ..." if claude plugin marketplace remove "$mkt" 2>/dev/null; then ok "Removed marketplace '$mkt'" else warn "Could not remove marketplace '$mkt'" fi done <<< "$AGENTKEY_MARKETS" fi fi fi # ── 4. Plugin + marketplace caches ──────────────────────────────────────── step "4. Plugin / marketplace caches" CACHE_HITS=() for d in "$HOME/.claude/plugins/cache"/agentkey \ "$HOME/.claude/plugins/cache"/agentkey-skill \ "$HOME/.claude/plugins/cache"/agentkey-*; do [ -d "$d" ] && CACHE_HITS+=("$d") done if [ -d "$HOME/.claude/plugins/marketplaces" ]; then for d in "$HOME/.claude/plugins/marketplaces"/*agentkey*; do [ -d "$d" ] && CACHE_HITS+=("$d") done fi if [ ${#CACHE_HITS[@]} -eq 0 ]; then skipped "No cache found" else for d in "${CACHE_HITS[@]}"; do rm -rf "$d" && ok "Removed $d" done fi # ── 5. Shell RC environment exports (legacy) ────────────────────────────── step "5. Shell environment exports (legacy)" changed=false for RC in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do [ -f "$RC" ] || continue if grep -q "agentkey-env-start" "$RC" 2>/dev/null; then if have_python; then py - "$RC" <<'EOF' import re, sys p = sys.argv[1] c = open(p).read() n = re.sub(r'\n# agentkey-env-start.*?# agentkey-env-end', '', c, flags=re.DOTALL) if n != c: open(p, 'w').write(n) EOF ok "Cleaned agentkey env block from $RC" changed=true else warn "python not found — edit $RC manually (look for 'agentkey-env-start')" fi fi done $changed || skipped "No agentkey env block in any shell RC" # ── 6. CLAUDE.md cleanup (legacy) ───────────────────────────────────────── step "6. CLAUDE.md sections (legacy)" md_changed=false for CLAUDE_MD in "$HOME/.claude/CLAUDE.md" ".claude/CLAUDE.md" "CLAUDE.md"; do [ -f "$CLAUDE_MD" ] || continue grep -q "AgentKey\|agentkey\|AGENTKEY" "$CLAUDE_MD" 2>/dev/null || continue if have_python; then OUT=$(py - "$CLAUDE_MD" <<'EOF' import re, sys p = sys.argv[1] c = open(p).read() c2 = re.sub(r'\n# AgentKey\n.*?(?=\n# |\Z)', '', c, flags=re.DOTALL) c2 = re.sub(r'\n[^\n]*(\.agentkey|agentkey.*activation\.md|agentkey.*SKILL\.md)[^\n]*', '', c2, flags=re.IGNORECASE) if c2 != c: open(p, 'w').write(c2) print("CHANGED") else: print("NO_MATCH") EOF ) if [ "$OUT" = "CHANGED" ]; then ok "Removed AgentKey section from $CLAUDE_MD" md_changed=true fi else warn "python not found — edit $CLAUDE_MD manually" fi done $md_changed || skipped "No removable AgentKey section in CLAUDE.md" # ── 7. npm / npx caches ─────────────────────────────────────────────────── step "7. npm / npx caches" if command -v npm >/dev/null 2>&1; then _global_list="$(npm list -g --depth=0 2>/dev/null || true)" _removed_any=false # Sweep both the current package name and the legacy v0.x name so # users who installed before the @agentkey/mcp → @agentkey/cli rename # get a clean uninstall. for _pkg in "@agentkey/cli" "@agentkey/mcp"; do if echo "$_global_list" | grep -q "$_pkg"; then info "Uninstalling global $_pkg ..." if npm uninstall -g "$_pkg" >/dev/null 2>&1; then ok "Removed $_pkg" _removed_any=true else warn "Could not remove $_pkg — try: npm uninstall -g $_pkg" fi fi done if ! $_removed_any; then skipped "No global @agentkey/cli or @agentkey/mcp installed" fi else skipped "npm not on PATH" fi if [ -d "$HOME/.npm/_npx" ]; then NPX_HITS=$(find "$HOME/.npm/_npx" -maxdepth 3 -type d -iname "*agentkey*" 2>/dev/null || true) if [ -n "$NPX_HITS" ]; then echo "$NPX_HITS" | xargs rm -rf ok "Cleared agentkey entries from npx cache" else skipped "No agentkey entries in npx cache" fi else skipped "No ~/.npm/_npx directory" fi # ── 7b. AgentKey config dir (snooze/disable/telemetry state) ───────────── step "7b. AgentKey config directory" AGENTKEY_CFG="$HOME/.config/agentkey" if [ -d "$AGENTKEY_CFG" ]; then rm -rf "$AGENTKEY_CFG" && ok "Removed $AGENTKEY_CFG" else skipped "No $AGENTKEY_CFG directory" fi # ── 8. Residual artifacts ───────────────────────────────────────────────── step "8. Residual artifacts" # 8a. MCP stdio log (macOS only; Claude Desktop path) MCP_LOG_MAC="$HOME/Library/Logs/Claude/mcp-server-agentkey.log" if [ -f "$MCP_LOG_MAC" ]; then rm -f "$MCP_LOG_MAC" && ok "Removed $MCP_LOG_MAC" fi # 8b. Plugin registry JSONs — strip any agentkey-keyed entries if have_python; then for REG in "$HOME/.claude/plugins/installed_plugins.json" \ "$HOME/.claude/plugins/known_marketplaces.json" \ "$HOME/.claude/mcp-needs-auth-cache.json"; do [ -f "$REG" ] || continue RESULT=$(py - "$REG" <<'EOF' import json, sys p = sys.argv[1] try: with open(p) as f: d = json.load(f) except Exception: sys.exit(0) def scrub(obj): removed = 0 if isinstance(obj, dict): for k in list(obj.keys()): if 'agentkey' in k.lower(): del obj[k]; removed += 1 else: removed += scrub(obj[k]) elif isinstance(obj, list): kept = [] for item in obj: s = json.dumps(item).lower() if not isinstance(item, str) else item.lower() if 'agentkey' in s: removed += 1 else: removed += scrub(item); kept.append(item) obj[:] = kept return removed n = scrub(d) if n: with open(p, 'w') as f: json.dump(d, f, indent=2) print(n) EOF ) if [[ "$RESULT" =~ ^[0-9]+$ ]] && [ "$RESULT" -gt 0 ]; then ok "Cleaned $RESULT entry/entries from $REG" fi done fi # ── Done ────────────────────────────────────────────────────────────────── if $DSH_CLEANED; then printf "\n ${BOLD}✓ Uninstall complete.${NC} Running DSH profiles watch removal; close legacy sessions or restart once if needed.\n\n" else printf "\n ${BOLD}✓ Uninstall complete.${NC} Restart your agent to apply changes.\n\n" fi