#!/usr/bin/env bash # uninstall-larry.sh — cleanly and SECURELY remove everything Larry-Anywhere # (a.k.a. "Cloverleaf-Larry") put down, in ONE run. Healthcare-grade: PHI-bearing # files are securely shredded (where the platform allows) BEFORE the dir is wiped. # # Reverses the installer EXACTLY: it removes only what Larry-Anywhere created # (its install dir / $LARRY_HOME, the `larry` PATH shims, the optional PHI venv, # the jq fallback binary, all runtime/PHI artifacts) and NOTHING the installer # didn't create. It does NOT touch your Cloverleaf sites or your $HCIROOT. It # DOES (by default) strip Larry-related credential exports it finds in your shell # rc — with a timestamped backup first — because leaving plaintext secrets behind # is the unsafe default for a healthcare deployment (override with --keep-rc). # # Default is a DRY-RUN preview. You must pass --yes (or confirm at the prompt) # to actually delete. Every path removed is printed, per-step success/failure. # # Usage: # uninstall-larry.sh # dry-run: list exactly what WOULD happen # uninstall-larry.sh --dry-run # (explicit) same as above # uninstall-larry.sh --yes # remove without the interactive prompt # uninstall-larry.sh --keep-data # remove program files but KEEP user data # # (sessions/, journal/, lessons/, # # knowledge/, sanitize/, log/, creds) # uninstall-larry.sh --keep-rc # do NOT modify shell rc files; just PRINT # # any Larry credential lines for manual # # removal (no auto-strip) # uninstall-larry.sh --no-shred # skip secure-delete; plain rm (NOT advised # # for PHI hosts — kept for slow disks) # uninstall-larry.sh --help # # Also reachable as a subcommand: larry uninstall [flags] # # Env vars (mirror the installer so it targets the SAME footprint): # LARRY_HOME install location (default: $HOME/.larry) # LARRY_BIN_DIR primary `larry` shim dir (default: $HOME/bin) set -eu # ───────────────────────────────────────────────────────────────────────────── # Paths. NOTE on safety: we intentionally do NOT default LARRY_HOME to a bare # expansion that could collapse to "/". If HOME is empty AND LARRY_HOME is unset, # the guard below aborts rather than risk rm -rf on a dangerous path. # ───────────────────────────────────────────────────────────────────────────── LARRY_HOME="${LARRY_HOME:-${HOME:-}/.larry}" LARRY_BIN_DIR="${LARRY_BIN_DIR:-${HOME:-}/bin}" C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_GREEN=$'\033[32m' C_YELLOW=$'\033[33m'; C_RED=$'\033[31m'; C_CYAN=$'\033[36m'; C_DIM=$'\033[2m' say() { printf '%s%suninstall-larry>%s %s\n' "$C_CYAN" "$C_BOLD" "$C_RESET" "$*"; } ok() { printf ' %s\xe2\x9c\x93%s %s\n' "$C_GREEN" "$C_RESET" "$*"; } warn() { printf ' %s!%s %s\n' "$C_YELLOW" "$C_RESET" "$*"; } step() { printf '%s\xe2\x80\xa2%s %s\n' "$C_BOLD" "$C_RESET" "$*"; } die() { printf '%serror:%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; } # ───────────────────────────────────────────────────────────────────────────── # Platform detection (matches install-larry.sh / larry.sh). On Windows-bash # (MobaXterm/Cygwin/MSYS/Git-Bash) `shred` is usually ABSENT and there is no # Presidio venv — we degrade honestly. # ───────────────────────────────────────────────────────────────────────────── PLATFORM="unknown" case "$(uname -s 2>/dev/null || echo unknown)" in Linux*) PLATFORM="linux" ;; Darwin*) PLATFORM="darwin" ;; CYGWIN*|MINGW*|MSYS*) PLATFORM="windows-cygwin" ;; # MobaXterm lives here esac [ -n "${MSYSTEM:-}" ] && PLATFORM="windows-cygwin" case "${OSTYPE:-}" in cygwin*|msys*) PLATFORM="windows-cygwin" ;; esac HAVE_SHRED=0 command -v shred >/dev/null 2>&1 && HAVE_SHRED=1 # ───────────────────────────────────────────────────────────────────────────── # Args # ───────────────────────────────────────────────────────────────────────────── DRY_RUN=1 # default: preview only ASSUME_YES=0 KEEP_DATA=0 KEEP_RC=0 NO_SHRED=0 for arg in "${@:-}"; do case "$arg" in --yes|-y) DRY_RUN=0; ASSUME_YES=1 ;; --dry-run|-n) DRY_RUN=1 ;; --keep-data) KEEP_DATA=1 ;; --keep-rc) KEEP_RC=1 ;; --no-shred) NO_SHRED=1 ;; --help|-h) sed -n '2,49p' "$0"; exit 0 ;; "") : ;; *) die "unknown flag: $arg (try --help)" ;; esac done # ───────────────────────────────────────────────────────────────────────────── # HARD SAFETY GUARD — never operate on an empty/unset/root/HOME LARRY_HOME, so a # misconfigured env can never turn this into `rm -rf /` or wipe $HOME. This is # the single most important guard in the script. # ───────────────────────────────────────────────────────────────────────────── _norm() { printf '%s' "$1" | sed 's:/*$::'; } # strip trailing slashes # If HOME is empty/unset, LARRY_HOME defaulted to "/.larry" — a root-leaf path we # must NOT operate on. Require an explicit, absolute, non-root LARRY_HOME. if [ -z "${HOME:-}" ] && [ -z "${LARRY_HOME:-}" ]; then die "refusing to operate: HOME is unset and LARRY_HOME was not given. Set LARRY_HOME explicitly." fi _LH_NORM="$(_norm "$LARRY_HOME")" case "$_LH_NORM" in ""|"/"|"/root"|"/home"|"/Users"|"/usr"|"/etc"|"/var"|"/bin"|"/tmp"|"/.larry") die "refusing to operate: LARRY_HOME resolves to a dangerous/empty path ('$LARRY_HOME'). Set LARRY_HOME explicitly." ;; esac # Must be an absolute path with at least two path components (e.g. /home/u/.larry), # never a single-component-at-root like /.larry or /larry. case "$_LH_NORM" in /*/?*) : ;; *) die "refusing to operate: LARRY_HOME ('$LARRY_HOME') must be an absolute path below a home/parent dir." ;; esac if [ -n "${HOME:-}" ] && [ "$_LH_NORM" = "$(_norm "$HOME")" ]; then die "refusing to operate: LARRY_HOME equals \$HOME ('$LARRY_HOME'). That cannot be right." fi # Defense in depth: LARRY_HOME leaf must look like a Larry dir. case "$_LH_NORM" in */.larry|*larry*) : ;; *) die "refusing to operate: LARRY_HOME ('$LARRY_HOME') doesn't look like a Larry install dir." ;; esac # ───────────────────────────────────────────────────────────────────────────── # Where this script physically lives (for the self-uninstall step). When run as # $LARRY_HOME/uninstall-larry.sh it's inside the tree we delete; when run from a # separate larry-anywhere checkout we also remove that checkout. # ───────────────────────────────────────────────────────────────────────────── SELF_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo "")" # ───────────────────────────────────────────────────────────────────────────── # 1. STOP EVERYTHING — kill cleanly, tolerate absence. Two layers: # (a) pidfiles Larry wrote under $LARRY_HOME (precise — only a PID we own); # (b) pgrep+kill by command pattern (catches detached REPLs / keepalive loops # with no pidfile). We NEVER kill the uninstaller itself, its parent, or # any process whose command contains 'uninstall-larry'. # ───────────────────────────────────────────────────────────────────────────── stop_bg_proc() { # $1 = label, $2 = pidfile local label="$1" pidfile="$2" pid="" [ -f "$pidfile" ] || return 0 pid="$(tr -d '[:space:]' < "$pidfile" 2>/dev/null || echo "")" case "$pid" in ''|*[!0-9]*) return 0 ;; esac if kill -0 "$pid" 2>/dev/null; then if [ "$DRY_RUN" = "1" ]; then printf ' %s\xe2\x80\xa2%s would stop %s (PID %s)\n' "$C_YELLOW" "$C_RESET" "$label" "$pid" else kill "$pid" 2>/dev/null && ok "stopped $label (PID $pid)" \ || warn "could not stop $label (PID $pid) — kill it manually" fi fi } SELF_PID="$$" SELF_PPID="$(ps -o ppid= -p "$$" 2>/dev/null | tr -d '[:space:]' || echo '')" pkill_pattern() { # $1 = label, $2 = pgrep -f pattern local label="$1" pat="$2" command -v pgrep >/dev/null 2>&1 || return 0 # no pgrep → nothing safe to do local pids="" pid cmd pids="$(pgrep -f "$pat" 2>/dev/null || true)" for pid in $pids; do [ -n "$pid" ] || continue case "$pid" in *[!0-9]*) continue ;; esac [ "$pid" = "$SELF_PID" ] && continue [ -n "$SELF_PPID" ] && [ "$pid" = "$SELF_PPID" ] && continue cmd="$(ps -o args= -p "$pid" 2>/dev/null || echo '')" case "$cmd" in *uninstall-larry*) continue ;; esac # never kill ourselves if [ "$DRY_RUN" = "1" ]; then printf ' %s\xe2\x80\xa2%s would stop %s (PID %s)\n' "$C_YELLOW" "$C_RESET" "$label" "$pid" else kill "$pid" 2>/dev/null && ok "stopped $label (PID $pid)" \ || warn "could not stop $label (PID $pid) — kill it manually" fi done } say "1) Stopping Larry processes (best-effort, tolerates absence)" stop_bg_proc "PHI Presidio sidecar" "$LARRY_HOME/.phi-sidecar.pid" stop_bg_proc "reverse SSH tunnel" "$LARRY_HOME/tunnel.pid" # Patterns require a path-sep/space before 'larry.sh' so 'uninstall-larry.sh' # (this script) is NOT matched; the per-PID uninstall-larry exclusion is backup. pkill_pattern "larry.sh REPL" '[/ ]larry\.sh' pkill_pattern "phi-presidio-sidecar" 'phi-presidio-sidecar' pkill_pattern "larry-tunnel keepalive" 'larry-tunnel' echo "" # ───────────────────────────────────────────────────────────────────────────── # 2/3. Build removal lists, separating: # - PHI_TARGETS : cleartext-PHI files → SECURE delete first (healthcare) # - PROGRAM_TARGETS: program footprint + the rest of $LARRY_HOME # - DATA_TARGETS : user data preserved under --keep-data # ───────────────────────────────────────────────────────────────────────────── PHI_TARGETS=() PROGRAM_TARGETS=() DATA_TARGETS=() collect_phi() { [ -d "$LARRY_HOME" ] || return 0 local f for f in "$LARRY_HOME/log/auto-phi.log" "$LARRY_HOME/sanitize/lookup.tsv"; do [ -f "$f" ] && PHI_TARGETS+=("$f") done if [ -d "$LARRY_HOME/sessions" ]; then if command -v find >/dev/null 2>&1; then while IFS= read -r f; do [ -n "$f" ] && PHI_TARGETS+=("$f"); done \ < <(find "$LARRY_HOME/sessions" -type f -name '*.log.md' 2>/dev/null) else # find absent (stripped MobaXterm): glob fallback so session PHI is never # silently skipped. nullglob so a no-match doesn't add a literal pattern. local _ng; _ng="$(shopt -p nullglob 2>/dev/null || true)" shopt -s nullglob 2>/dev/null || true for f in "$LARRY_HOME"/sessions/*.log.md "$LARRY_HOME"/sessions/**/*.log.md; do [ -f "$f" ] && PHI_TARGETS+=("$f") done eval "$_ng" 2>/dev/null || true fi fi } [ "$KEEP_DATA" = "0" ] && collect_phi # Shims / copies. We only remove a `larry` file if it is OUR shim (auto-generated # header) OR a symlink/launcher pointing into $LARRY_HOME. is_our_shim() { # $1 = path local p="$1" [ -e "$p" ] || return 1 grep -q 'Auto-generated by install-larry.sh' "$p" 2>/dev/null && return 0 grep -q "$LARRY_HOME" "$p" 2>/dev/null && return 0 if [ -L "$p" ]; then case "$(readlink "$p" 2>/dev/null)" in *"$LARRY_HOME"*) return 0 ;; esac fi return 1 } _seen_shims="" for SHIM in "$LARRY_BIN_DIR/larry" \ "${HOME:-}/larry" "${HOME:-}/.local/bin/larry" "${HOME:-}/bin/larry"; do [ -n "$SHIM" ] || continue case "$_seen_shims" in *"|$SHIM|"*) continue ;; esac # dedupe (LARRY_BIN_DIR may == $HOME/bin) _seen_shims="$_seen_shims|$SHIM|" if is_our_shim "$SHIM"; then PROGRAM_TARGETS+=("$SHIM") elif [ -e "$SHIM" ]; then warn "$SHIM exists but is NOT our shim/launcher — leaving it untouched." fi done # A scp'd source folder (install-from-folder path). if [ -n "${HOME:-}" ] && [ -d "$HOME/larry-anywhere" ]; then PROGRAM_TARGETS+=("$HOME/larry-anywhere") fi # The install dir itself. if [ -d "$LARRY_HOME" ]; then if [ "$KEEP_DATA" = "0" ]; then PROGRAM_TARGETS+=("$LARRY_HOME") else for p in \ larry.sh larry-tunnel.sh larry-rollback.sh larry-auth.sh uninstall-larry.sh \ install-larry.sh VERSION MANUAL.md MANIFEST CHANGELOG.md README.md \ inbound-systems.tsv agents lib bin phi-venv \ .last-sync-version .origin .oauth-optin-warned \ .phi-notice-shown .b64-py3-notice-shown \ .last-curl-rc .last-curl-stderr .last-curl-headers \ .last-stream-headers .last-stream-curlerr \ .phi-sidecar.pid tunnel.pid tunnel.url tunnel.log .history; do [ -e "$LARRY_HOME/$p" ] && PROGRAM_TARGETS+=("$LARRY_HOME/$p") done for d in sessions journal lessons knowledge sanitize regression log \ .api-key .env .oauth.json .ssh-creds .ssh-sockets .ssh-hosts.tsv \ known_hosts .headers-sync-offset .headers-sync-state; do [ -e "$LARRY_HOME/$d" ] && DATA_TARGETS+=("$LARRY_HOME/$d") done fi fi # ───────────────────────────────────────────────────────────────────────────── # 5. Shell-profile credential lines. SAFE, PREDICTABLE behavior: # default = back up the rc (timestamped .larry-uninstall.bak) then STRIP the # matching lines; --keep-rc = print only, change nothing. # ───────────────────────────────────────────────────────────────────────────── RC_FILES=() for rc in "${HOME:-}/.bashrc" "${HOME:-}/.bash_profile" "${HOME:-}/.profile"; do [ -n "$rc" ] && [ -f "$rc" ] && RC_FILES+=("$rc") done CRED_RE='ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|LARRY_[A-Z_]*|GITEA_TOKEN' # ───────────────────────────────────────────────────────────────────────────── # Preview # ───────────────────────────────────────────────────────────────────────────── if [ "${#PROGRAM_TARGETS[@]}" -eq 0 ] && [ "${#PHI_TARGETS[@]}" -eq 0 ]; then say "nothing to remove — no Larry-Anywhere install footprint found." say " (looked for LARRY_HOME=$LARRY_HOME and shims under \$HOME)" fi say "2/3) Files to remove (PHI files securely shredded first):" echo "" if [ "${#PHI_TARGETS[@]}" -gt 0 ]; then printf '%sCleartext-PHI files (SECURE delete):%s\n' "$C_BOLD" "$C_RESET" for t in "${PHI_TARGETS[@]}"; do printf ' %sshred%s %s\n' "$C_RED" "$C_RESET" "$t"; done echo "" fi printf '%sProgram/footprint (removed):%s\n' "$C_BOLD" "$C_RESET" if [ "${#PROGRAM_TARGETS[@]}" -eq 0 ]; then printf ' %s(none)%s\n' "$C_DIM" "$C_RESET"; fi for t in "${PROGRAM_TARGETS[@]}"; do if [ -d "$t" ]; then printf ' %s(dir) %s%s\n' "$C_DIM" "$t" "$C_RESET" else printf ' %s\n' "$t"; fi done if [ "$KEEP_DATA" = "1" ]; then echo "" printf '%sUser data PRESERVED (--keep-data):%s\n' "$C_BOLD" "$C_RESET" if [ "${#DATA_TARGETS[@]}" -eq 0 ]; then printf ' %s(none found)%s\n' "$C_DIM" "$C_RESET" else for d in "${DATA_TARGETS[@]}"; do printf ' %skept%s %s\n' "$C_GREEN" "$C_RESET" "$d"; done; fi fi # Secure-delete capability honesty (esp. Windows/MobaXterm). echo "" if [ "$NO_SHRED" = "1" ]; then warn "secure-delete DISABLED (--no-shred): PHI files will be removed with plain rm." elif [ "$HAVE_SHRED" = "1" ]; then say "secure-delete: 'shred -u' available -> PHI files will be cryptographically shredded." else say "secure-delete: 'shred' NOT found on this platform ($PLATFORM) -> falling back to" say " best-effort overwrite-then-remove. NOTE: on Windows/MobaXterm and on copy-on-write" say " or SSD/flash filesystems, overwrite-in-place does NOT guarantee the original blocks" say " are gone. This will be reported per-file as 'overwrite (best-effort)'." fi # Shell rc preview echo "" say "5) Shell-profile credential lines:" RC_HITS=0 for rc in "${RC_FILES[@]:-}"; do [ -n "$rc" ] || continue hits="$(grep -nE "$CRED_RE" "$rc" 2>/dev/null || true)" if [ -n "$hits" ]; then RC_HITS=1 printf ' %s%s%s:\n' "$C_BOLD" "$rc" "$C_RESET" printf '%s\n' "$hits" | sed 's/^/ /' fi done if [ "$RC_HITS" = "0" ]; then printf ' %s(no Larry credential exports found in .bashrc/.bash_profile/.profile)%s\n' "$C_DIM" "$C_RESET" elif [ "$KEEP_RC" = "1" ]; then say " --keep-rc: the lines above will be PRINTED ONLY — remove them by hand." else say " default: a timestamped backup will be written, then these lines auto-stripped." fi echo "" if [ "$DRY_RUN" = "1" ]; then say "${C_BOLD}DRY-RUN${C_RESET} — nothing was changed. Re-run with ${C_CYAN}--yes${C_RESET} to execute the above." exit 0 fi # ───────────────────────────────────────────────────────────────────────────── # Confirm # ───────────────────────────────────────────────────────────────────────────── if [ "$ASSUME_YES" != "1" ]; then printf '%sProceed with uninstall? This cannot be undone. [y/N]: %s' "$C_YELLOW" "$C_RESET" ans="" if [ -t 0 ]; then read -r ans /dev/null && echo "plain-rm" || echo "FAILED"; return 0; fi if [ "$HAVE_SHRED" = "1" ]; then if shred -u -z -n 3 "$f" 2>/dev/null; then echo "shred"; return 0; fi if shred -u "$f" 2>/dev/null; then echo "shred"; return 0; fi fi # Fallback: overwrite-in-place then remove. Best-effort on Windows/CoW/SSD. local sz sz="$(wc -c < "$f" 2>/dev/null || echo 0)" if [ "${sz:-0}" -gt 0 ] 2>/dev/null; then if command -v dd >/dev/null 2>&1; then dd if=/dev/zero of="$f" bs=1 count="$sz" conv=notrunc 2>/dev/null || true [ -r /dev/urandom ] && dd if=/dev/urandom of="$f" bs=1 count="$sz" conv=notrunc 2>/dev/null || true else : > "$f" 2>/dev/null || true # truncate at least fi fi rm -f "$f" 2>/dev/null && echo "overwrite" || echo "FAILED" } if [ "${#PHI_TARGETS[@]}" -gt 0 ]; then step "Securely deleting cleartext-PHI files" phi_shredded=0; phi_besteffort=0; phi_failed=0 for f in "${PHI_TARGETS[@]}"; do method="$(secure_delete "$f")" case "$method" in shred) ok "shredded (secure): $f"; phi_shredded=$((phi_shredded+1)) ;; overwrite) warn "overwrite-then-rm (best-effort, NOT guaranteed on this FS): $f"; phi_besteffort=$((phi_besteffort+1)) ;; plain-rm) warn "plain rm (--no-shred): $f"; phi_besteffort=$((phi_besteffort+1)) ;; absent) : ;; *) warn "FAILED to remove PHI file (remove by hand!): $f"; phi_failed=$((phi_failed+1)) ;; esac done echo "" if [ "$HAVE_SHRED" = "1" ] && [ "$NO_SHRED" = "0" ]; then say "PHI secure-delete: $phi_shredded shredded, $phi_besteffort best-effort, $phi_failed failed." else say "PHI delete on $PLATFORM: secure shred UNAVAILABLE — $phi_besteffort file(s) removed best-effort," say " $phi_failed failed. Treat the underlying disk as potentially still containing PHI remnants;" say " follow your data-handling policy for media sanitization if required." fi echo "" fi # ───────────────────────────────────────────────────────────────────────────── # 2/4. Remove the rest of the footprint (program files, shims, copies, the dir). # Scoped strictly to the built list — no glob expansion, no parent touch. # ───────────────────────────────────────────────────────────────────────────── removed=0; failed=0 if [ "${#PROGRAM_TARGETS[@]}" -gt 0 ]; then step "Removing program files, shims and the install dir" for t in "${PROGRAM_TARGETS[@]}"; do if rm -rf "$t" 2>/dev/null; then ok "removed $t"; removed=$((removed+1)) else warn "could not remove $t (check permissions; remove by hand)"; failed=$((failed+1)); fi done say "removed $removed item(s); $failed failed." echo "" fi # ───────────────────────────────────────────────────────────────────────────── # 5. Strip Larry credential lines from shell rc (default), with backup. Skipped # under --keep-rc (already printed for manual removal in the preview). # ───────────────────────────────────────────────────────────────────────────── if [ "$KEEP_RC" = "0" ] && [ "${#RC_FILES[@]}" -gt 0 ]; then step "Stripping Larry credential exports from shell rc (backup first)" ts="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo bak)" for rc in "${RC_FILES[@]}"; do if grep -qE "$CRED_RE" "$rc" 2>/dev/null; then cp -p "$rc" "$rc.larry-uninstall.$ts.bak" 2>/dev/null \ && ok "backed up $rc -> $rc.larry-uninstall.$ts.bak" \ || warn "could not back up $rc — leaving it UNCHANGED for safety" if [ -f "$rc.larry-uninstall.$ts.bak" ]; then if grep -vE "$CRED_RE" "$rc" > "$rc.larry-tmp" 2>/dev/null && mv "$rc.larry-tmp" "$rc" 2>/dev/null; then ok "stripped Larry credential lines from $rc" else rm -f "$rc.larry-tmp" 2>/dev/null || true warn "could not rewrite $rc — remove the credential lines by hand" fi fi fi done echo "" fi # ───────────────────────────────────────────────────────────────────────────── # 6. SELF-UNINSTALL LAST. If this script ran from a separate larry-anywhere # checkout (not inside $LARRY_HOME, which is already gone), remove that too — # but ONLY if we actually performed an uninstall this run AND the dir holds a # full bundle. Then remove this script file itself. # ───────────────────────────────────────────────────────────────────────────── step "Self-uninstall (removing the larry-anywhere install dir / this script)" DID_WORK=0 [ "$removed" -gt 0 ] && DID_WORK=1 [ "${#PHI_TARGETS[@]}" -gt 0 ] && DID_WORK=1 if [ -n "$SELF_DIR" ] && [ -d "$SELF_DIR" ]; then case "$(_norm "$SELF_DIR")" in "$_LH_NORM") : ;; # was inside LARRY_HOME, already removed above */larry-anywhere|*cloverleaf-larry*|*/.larry) if [ "$DID_WORK" = "1" ] && { [ -f "$SELF_DIR/larry.sh" ] || [ -f "$SELF_DIR/install-larry.sh" ]; }; then rm -rf "$SELF_DIR" 2>/dev/null && ok "removed self checkout: $SELF_DIR" \ || warn "could not remove $SELF_DIR — remove by hand" else warn "left $SELF_DIR in place (nothing uninstalled this run, or not a full bundle)." fi ;; *) warn "running from $SELF_DIR — not a recognized Larry dir, NOT self-removing it." ;; esac fi # Remove this script file itself if it still exists. if [ -f "$0" ]; then rm -f "$0" 2>/dev/null && ok "removed $0" || true; fi echo "" # ───────────────────────────────────────────────────────────────────────────── # FINAL "confirm gone" check # ───────────────────────────────────────────────────────────────────────────── step "Confirming removal" gone=1 if [ -e "$LARRY_HOME" ] && [ "$KEEP_DATA" = "0" ]; then warn "$LARRY_HOME still exists"; gone=0; else ok "$LARRY_HOME removed"; fi if command -v larry >/dev/null 2>&1; then warn "a 'larry' command is still on PATH ($(command -v larry)) — if it isn't ours, that's fine" else ok "no 'larry' command on PATH" fi [ "$gone" = "1" ] && say "${C_GREEN}uninstall complete — local footprint gone.${C_RESET}" \ || say "${C_YELLOW}uninstall finished with leftovers — see warnings above.${C_RESET}" echo "" # ───────────────────────────────────────────────────────────────────────────── # 7. POST-UNINSTALL REMINDER — non-negotiable for a healthcare deployment. # Local removal does NOT invalidate credentials that may have egressed. # ───────────────────────────────────────────────────────────────────────────── printf '%s%s================ IMPORTANT: FINISH AT THE SOURCE ================%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" cat <<'EOF' Removing the files here does NOT revoke the credentials. Any copy that already egressed (e.g. exported in another shell, scp'd, or cached elsewhere) remains live until you revoke it AT ANTHROPIC. Do BOTH now: 1. Anthropic Console -> Settings -> API Keys: DELETE/REVOKE the API key minted for this machine (do not just disable). https://console.anthropic.com 2. Anthropic Console -> account security / Connected apps / sessions: REVOKE the Claude-Code OAuth authorization grant and "sign out everywhere" (a .oauth.json refresh token can still mint access tokens until revoked). 3. Revoke any Gitea PAT (LARRY_GITEA_TOKEN / GITEA_TOKEN) that was set, and rotate any SSH credential this box held to your Mac / hop host. HEALTHCARE / PHI / BAA reminder: This agent ran on an HL7 integration engine and had a path to PHI. It may have transmitted PHI to a third party (Anthropic) and stored cleartext PHI locally (now deleted). Review the engagement BAA and, absent clear evidence no PHI ever flowed, treat client notification / PHI-disclosure as a decision you must make deliberately — do not assume local deletion closes the obligation. EOF printf '%s%s=================================================================%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET"