Skip to content

Bug: Uninstall wizard doesn't clean up shell env vars, Docker containers, or CLI tools #142

@udhaya10

Description

@udhaya10

Description

The uninstall wizard (--uninstall) correctly archives ~/.claude and restores the backup, but leaves several artifacts behind:

1. Dangling CLAUDE_OPC_DIR in shell config

The install wizard adds export CLAUDE_OPC_DIR=... to ~/.zshrc or ~/.bashrc (wizard.py line ~852), but the uninstall never removes it.

Install code that adds it:

content = shell_config.read_text()
export_line = f'export CLAUDE_OPC_DIR="{opc_dir}"'
if "CLAUDE_OPC_DIR" not in content:
with open(shell_config, "a") as f:
f.write(f"\n# Continuous-Claude OPC directory (for skills to find scripts)\n{export_line}\n")
console.print(f" [green]OK[/green] Added CLAUDE_OPC_DIR to {shell_config.name}")
else:
console.print(f" [dim]CLAUDE_OPC_DIR already in {shell_config.name}[/dim]")

Uninstall code (no cleanup):

def uninstall_opc_integration(
project_dir: Path | None = None,
is_global: bool = False,
) -> dict[str, Any]:
"""Uninstall OPC integration and restore from backup.
This function:
1. Archives current .claude to .claude-v3.archived.<timestamp>
2. Restores from the most recent .claude.backup.* if available
3. Preserves user data (history, MCP configs, etc.) by copying to restored dir
Args:
project_dir: Project directory (uses cwd if None)
is_global: If True, operate on global ~/.claude
Returns:
dict with keys:
- success: bool
- archived_to: Path where current .claude was moved
- restored_from: Path that was restored (or None)
- preserved: List of preserved files/dirs
- message: Human-readable summary
"""
if is_global:
claude_dir = get_global_claude_dir()
else:
claude_dir = get_claude_dir(project_dir)
result: dict[str, Any] = {
"success": False,
"archived_to": None,
"restored_from": None,
"preserved": [],
"message": "",
}
if not claude_dir.exists():
result["message"] = "No .claude directory found - nothing to uninstall"
result["success"] = True
return result
# Step 1: Collect files to preserve BEFORE archiving
preserved_data: dict[str, Path] = {}
for filename in PRESERVE_FILES:
src = claude_dir / filename
if src.exists():
preserved_data[filename] = src
for dirname in PRESERVE_DIRS:
src = claude_dir / dirname
if src.exists() and src.is_dir():
preserved_data[dirname] = src
# Step 2: Archive current .claude
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
archive_path = claude_dir.parent / f".claude-v3.archived.{timestamp}"
try:
shutil.move(str(claude_dir), str(archive_path))
result["archived_to"] = archive_path
except Exception as e:
result["message"] = f"Failed to archive current .claude: {e}"
return result
# Step 3: Find and restore from backup
backup = find_latest_backup(archive_path.parent / ".claude")
if backup is None:
backup = find_latest_backup(archive_path)
if backup and backup.exists():
try:
shutil.copytree(backup, claude_dir)
result["restored_from"] = backup
except Exception as e:
result["message"] = (
f"Archived v3 to {archive_path.name}, but restore failed: {e}\n"
f" Manual restore: cp -r {backup} {claude_dir}"
)
result["success"] = True
return result
else:
# No backup - create empty .claude
claude_dir.mkdir(parents=True, exist_ok=True)
# Step 4: Preserve user data by copying from archive to restored dir
for name, src_path in preserved_data.items():
archived_src = archive_path / name
dest = claude_dir / name
if archived_src.exists() and not dest.exists():
try:
if archived_src.is_dir():
shutil.copytree(archived_src, dest)
else:
shutil.copy2(archived_src, dest)
result["preserved"].append(name)
except Exception:
pass # Best effort
# Build summary message
msg_parts = ["Uninstalled successfully."]
msg_parts.append(f" Archived v3 to: {archive_path.name}")
if result["restored_from"]:
msg_parts.append(f" Restored from: {backup.name}")
else:
msg_parts.append(" No backup found (created empty .claude)")
if result["preserved"]:
msg_parts.append(f" Preserved user data: {', '.join(result['preserved'])}")
result["message"] = "\n".join(msg_parts)
result["success"] = True
return result

2. Docker containers keep running

The install starts PostgreSQL and Redis containers (Step 6). The uninstall doesn't offer to stop them. After uninstall, orphaned containers keep running consuming resources.

3. CLI tools remain installed

The install puts tldr on PATH via uv tool install llm-tldr (Step 10). Uninstall doesn't remove it.

4. settings.local.json not in PRESERVE_FILES

PRESERVE_FILES = [
    "history.jsonl",
    "mcp_config.json",
    ".env",
    "projects.json",
]

Missing: settings.local.json — where users store custom permissions and local overrides.

Link:

PRESERVE_FILES = [
"history.jsonl", # Command history
"mcp_config.json", # MCP server configs
".env", # API keys and settings
"projects.json", # Project configs
]
PRESERVE_DIRS = [
"file-history", # File edit history
"projects", # Project-specific data
]

5. Silent failure on file preservation

except Exception:
    pass  # Best effort

If preserving history.jsonl fails, the user gets no warning.

Link:

except Exception:
pass # Best effort

Proposed Fix

Add cleanup steps to run_uninstall_wizard():

# 1. Remove CLAUDE_OPC_DIR from shell config
for shell_config in [Path.home() / ".zshrc", Path.home() / ".bashrc"]:
    if shell_config.exists():
        content = shell_config.read_text()
        if "CLAUDE_OPC_DIR" in content:
            lines = [l for l in content.splitlines() 
                     if "CLAUDE_OPC_DIR" not in l and "Continuous-Claude OPC" not in l]
            shell_config.write_text("\n".join(lines) + "\n")

# 2. Offer to stop Docker containers
if Confirm.ask("Stop CC-v3 Docker containers (PostgreSQL, Redis)?", default=True):
    subprocess.run(["docker", "compose", "-f", "docker/docker-compose.yml", "down"])

# 3. Offer to remove CLI tools
if Confirm.ask("Remove TLDR CLI?", default=False):
    subprocess.run(["uv", "tool", "uninstall", "llm-tldr"])

# 4. Add settings.local.json to PRESERVE_FILES
PRESERVE_FILES.append("settings.local.json")

# 5. Warn on preserve failures
except Exception as e:
    console.print(f"  [yellow]WARN[/yellow] Could not preserve {name}: {e}")

Impact

Artifact Left Behind After Uninstall
export CLAUDE_OPC_DIR=... in shell rc Yes — dangling env var
Docker containers (postgres, redis) Yes — consuming resources
tldr CLI on PATH Yes — minor
settings.local.json Lost if not in backup

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions