cloverleaf-larry/uninstall-larry.sh
bj 6b45543652 v0.8.34: harden uninstall-larry.sh into a first-class PHI-grade decommission
One-run `larry uninstall` / uninstall-larry.sh that:
- stops detached larry.sh REPL + phi-presidio-sidecar + larry-tunnel
  (pgrep+kill by pattern, never kills itself/parent/uninstall-larry)
- SECURELY deletes cleartext PHI (auto-phi.log, lookup.tsv, sessions/*.log.md)
  via shred -u -z -n 3, with overwrite-then-rm fallback on Windows/MobaXterm
  where shred is absent, honest per-platform "secure achieved?" reporting,
  and a find-less bash-glob fallback for session files
- strips ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|LARRY_*|GITEA_TOKEN from
  shell rc with a timestamped backup (default), or prints them under --keep-rc
- removes ~/larry, ~/.local/bin/larry, ~/bin/larry, ~/larry-anywhere (our shims
  only; foreign `larry` preserved), then self-removes a standalone checkout
- prints a FINISH-AT-THE-SOURCE reminder: revoke API key + OAuth grant + PAT,
  plus a BAA/PHI-disclosure note
- hard rm-rf-/ guards (empty/unset/root/$HOME/non-larry LARRY_HOME refused),
  scoped strictly to the built target list; DRY-RUN default; new --keep-rc and
  --no-shred flags

Tested: full real run, dry-run scope, all rm-rf guards, --keep-data,
no-shred(Windows) fallback, idempotency, standalone-checkout self-uninstall.
MANIFEST regenerated so the self-update ships it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:52:24 -07:00

524 lines
29 KiB
Bash
Executable File

#!/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/tty || ans=""; else read -r ans || ans=""; fi
case "$ans" in y|Y|yes|YES) : ;; *) say "aborted — nothing changed."; exit 0 ;; esac
fi
echo ""
# ─────────────────────────────────────────────────────────────────────────────
# 3. SECURE-DELETE the cleartext-PHI files FIRST, with honest per-file reporting.
# ─────────────────────────────────────────────────────────────────────────────
secure_delete() { # $1 = file. echoes the method actually achieved.
local f="$1"
[ -f "$f" ] || { echo "absent"; return 0; }
if [ "$NO_SHRED" = "1" ]; then rm -f "$f" 2>/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"