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>
This commit is contained in:
bj 2026-05-31 18:52:24 -07:00
parent 7606a535c9
commit 6b45543652
4 changed files with 465 additions and 100 deletions

View File

@ -4,6 +4,54 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here.
Versioning is loose-semver; bumps trigger the in-process self-update on every
running client via `LARRY_BASE_URL` + `MANIFEST`.
## v0.8.34 — 2026-05-31
**Hardened `uninstall-larry.sh` into a first-class, healthcare-grade decommission
command — "update larry, then run one command."**
The v0.8.33 uninstaller removed the install footprint but did not meet the
operational decommission requirements for a PHI-handling deployment left on a
client box. This release makes `larry uninstall` (and the shipped
`uninstall-larry.sh` it delegates to) self-contained and idempotent: one run
stops everything, securely destroys cleartext PHI, scrubs creds, self-removes,
and tells you what you still must do at the source. The `larry uninstall`
early-dispatch in `larry.sh` is unchanged — it already delegates here.
### New / changed behavior
- **Stop everything (tolerant):** in addition to the precise pidfile kills (PHI
Presidio sidecar, reverse-SSH tunnel), it now `pgrep`+kills detached
`larry.sh` REPLs, `phi-presidio-sidecar`, and `larry-tunnel` keepalives by
command pattern. It NEVER kills itself, its parent, or any `uninstall-larry`
process (patterns + per-PID exclusion).
- **PHI hygiene (healthcare):** `log/auto-phi.log`, `sanitize/lookup.tsv`, and
`sessions/*.log.md` are SECURELY deleted FIRST — `shred -u -z -n 3` where
available; overwrite-then-rm fallback (zero + urandom passes) where `shred` is
absent (Windows/MobaXterm); truncate-then-rm if even `dd` is missing. It
prints HONESTLY per-file whether a real secure-delete was achieved, and warns
that overwrite-in-place is not guaranteed on SSD/CoW/Windows filesystems.
`find`-less hosts use a bash-glob fallback so session PHI is never skipped.
- **Shell-profile creds:** greps `.bashrc`/`.bash_profile`/`.profile` for
`ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|LARRY_*|GITEA_TOKEN` and, by
default, backs up the rc (timestamped `.larry-uninstall.<ts>.bak`) then strips
only those lines. `--keep-rc` prints them for manual removal instead.
- **More shim/copy locations:** removes `~/larry`, `~/.local/bin/larry`,
`~/bin/larry`, `$LARRY_BIN_DIR/larry`, and a scp'd `~/larry-anywhere` — but
only OUR shims (auto-generated header or a launcher/symlink into `$LARRY_HOME`);
a foreign `larry` is left untouched.
- **Self-uninstall last:** when run from a standalone larry-anywhere checkout
(not from inside `$LARRY_HOME`), removes that checkout and the script itself —
only if real work was done this run and the dir is a full bundle.
- **Post-uninstall reminder:** prints the explicit "FINISH AT THE SOURCE" block —
REVOKE the Anthropic API key AND the Claude-Code OAuth grant (local removal
does not invalidate egressed copies), revoke any Gitea PAT / rotate SSH creds,
plus a BAA/PHI-disclosure reminder for healthcare deployments.
- **Safety guards:** refuses to operate if `LARRY_HOME` is empty/unset, `/`,
`$HOME`, a single-component-at-root path (`/.larry`), or anything that doesn't
look like a Larry dir — so a misconfigured env can never `rm -rf /`. Removal is
scoped strictly to the built target list (no glob expansion, no parent touch).
- New flags: `--keep-rc`, `--no-shred` (alongside the existing `--yes`,
`--dry-run`, `--keep-data`). DRY-RUN remains the default.
## v0.8.33 — 2026-05-29
**Two operator-requested features: a real uninstaller, and a deterministic-only

View File

@ -28,12 +28,12 @@ larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
install-larry.sh 0779dd74c0cecef174edbe7782da275be9bf6d2ec7e6ae88c1de46c666adc8b2
uninstall-larry.sh 5d0e2fe1366c818dc207f70c85e38279218b58f5eae10a8a6b0a277a02f2e4d0
uninstall-larry.sh c53ad2d8354c7adeb243b541f027f3f481e4a8661eecfd7af14d7ca53cfcaad9
# Metadata
VERSION d9d12c30d3607edd1bfd2ef0feb0738f917e113a4bc1570d5dbb2ea8426531a8
VERSION 437e52707aa48e9ee29b4469dc0790ad646390c08644c415a5171052487fd280
MANUAL.md 5ff54d6d5fae826f8b3da1eb3be6476076bb15f9b1417a4de285e59ea37e1b1f
CHANGELOG.md 67ed1238990e96d481ffa804cbfcbf219958438d6285e6efe864a33b8df97a32
CHANGELOG.md 25b82ebf360bb345bb1d7008386532773e9bf55d67b331a7d572d63ff4ab87ae
# Agent personas (system-prompt overlays)
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1

View File

@ -1 +1 @@
0.8.33
0.8.34

View File

@ -1,109 +1,257 @@
#!/usr/bin/env bash
# uninstall-larry.sh — cleanly remove everything install-larry.sh put down.
# 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, the `larry` PATH shim, the optional PHI venv, the jq
# fallback binary, and all runtime artifacts under $LARRY_HOME) and NOTHING the
# installer didn't create. It does NOT touch your shell rc, your Cloverleaf
# sites, your $HCIROOT, or any file outside the install footprint.
# (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.
# to actually delete. Every path removed is printed, per-step success/failure.
#
# Usage:
# uninstall-larry.sh # dry-run: list exactly what WOULD be removed
# 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 [--dry-run|--yes|--keep-data]
# 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 where the `larry` shim was symlinked (default: $HOME/bin)
# LARRY_BIN_DIR primary `larry` shim dir (default: $HOME/bin)
set -eu
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
LARRY_BIN_DIR="${LARRY_BIN_DIR:-$HOME/bin}"
# ─────────────────────────────────────────────────────────────────────────────
# 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%s %s\n' "$C_GREEN" "$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 ;;
--help|-h) sed -n '2,38p' "$0"; exit 0 ;;
--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
# ─────────────────────────────────────────────────────────────────────────────
# Stop background processes Larry may have started, BEFORE deleting their files.
# Both PID files live under $LARRY_HOME. Best-effort: a stale/dead PID is fine.
# We only kill a PID we can read from Larry's own pidfile — never a guessed one.
# 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.
# ─────────────────────────────────────────────────────────────────────────────
stop_bg_proc() {
# $1 = human label, $2 = pidfile path
_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 ;; # empty or non-numeric → nothing safe to do
esac
case "$pid" in ''|*[!0-9]*) return 0 ;; esac
if kill -0 "$pid" 2>/dev/null; then
if [ "$DRY_RUN" = "1" ]; then
printf ' %s•%s would stop %s (PID %s)\n' "$C_YELLOW" "$C_RESET" "$label" "$pid"
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"
kill "$pid" 2>/dev/null && ok "stopped $label (PID $pid)" \
|| warn "could not stop $label (PID $pid) — kill it manually"
fi
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Build the removal list. We separate PROGRAM artifacts (always removed) from
# USER DATA (removed unless --keep-data). Everything here was created by the
# installer or by Larry at runtime — we never list anything else.
# ─────────────────────────────────────────────────────────────────────────────
PROGRAM_TARGETS=() # files/dirs that ARE the install footprint
DATA_TARGETS=() # user-generated data under $LARRY_HOME
SELF_PID="$$"
SELF_PPID="$(ps -o ppid= -p "$$" 2>/dev/null | tr -d '[:space:]' || echo '')"
# The shim the installer drops onto PATH. We only remove it if it is OUR shim
# (auto-generated header), so we never delete an unrelated `larry` someone else
# put there.
SHIM="$LARRY_BIN_DIR/larry"
if [ -f "$SHIM" ] && grep -q 'Auto-generated by install-larry.sh' "$SHIM" 2>/dev/null; then
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 auto-generated shim — leaving it untouched."
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 entire install dir is ours. Rather than enumerate every dotfile, we list
# $LARRY_HOME itself for the full uninstall. With --keep-data we instead remove
# the program files inside it and preserve the user-data subtrees.
# The install dir itself.
if [ -d "$LARRY_HOME" ]; then
if [ "$KEEP_DATA" = "0" ]; then
PROGRAM_TARGETS+=("$LARRY_HOME")
else
# Program files (shipped bundle + caches + state markers + the jq fallback
# + the optional PHI venv). Anything not in this list under $LARRY_HOME is
# treated as user data and preserved.
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 \
@ -115,7 +263,6 @@ if [ -d "$LARRY_HOME" ]; then
.phi-sidecar.pid tunnel.pid tunnel.url tunnel.log .history; do
[ -e "$LARRY_HOME/$p" ] && PROGRAM_TARGETS+=("$LARRY_HOME/$p")
done
# User data preserved under --keep-data (listed for transparency).
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
@ -124,18 +271,34 @@ if [ -d "$LARRY_HOME" ]; then
fi
fi
if [ "${#PROGRAM_TARGETS[@]}" -eq 0 ]; then
say "nothing to remove — no Larry-Anywhere install footprint found."
say " (looked for LARRY_HOME=$LARRY_HOME and shim $SHIM)"
exit 0
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
# ─────────────────────────────────────────────────────────────────────────────
say "Larry-Anywhere uninstall — install footprint to remove:"
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 ""
printf '%sProgram files (always removed):%s\n' "$C_BOLD" "$C_RESET"
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
@ -143,64 +306,218 @@ 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
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
echo ""
say "NOT touched: your shell rc (any PATH export you added stays — remove it by hand),"
say " your Cloverleaf sites / \$HCIROOT, and anything outside the list above."
echo ""
# Stop background procs (preview prints "would stop …"; real run kills them).
stop_bg_proc "PHI Presidio sidecar" "$LARRY_HOME/.phi-sidecar.pid"
stop_bg_proc "reverse SSH tunnel" "$LARRY_HOME/tunnel.pid"
# 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 removed. Re-run with ${C_CYAN}--yes${C_RESET} to delete the above."
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 (skipped with --yes... but --yes already set ASSUME_YES; this is the
# belt-and-suspenders path if someone sets DRY_RUN=0 by other means).
# Confirm
# ─────────────────────────────────────────────────────────────────────────────
if [ "$ASSUME_YES" != "1" ]; then
printf '%sProceed with removal? This cannot be undone. [y/N]: %s' "$C_YELLOW" "$C_RESET"
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 removed."; exit 0 ;;
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. Print every path as it goes. rm -rf is scoped strictly to the list we
# built above — we never expand globs or touch a parent dir.
# ─────────────────────────────────────────────────────────────────────────────
removed=0
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)"
fi
done
# 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 ""
say "removed $removed item(s)."
if [ "$KEEP_DATA" = "1" ] && [ "${#DATA_TARGETS[@]}" -gt 0 ]; then
say "your data is preserved under $LARRY_HOME — delete it manually if you want a full wipe."
# ─────────────────────────────────────────────────────────────────────────────
# 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
# Reminder: the installer never EDITS your shell rc; it only WARNS you to add a
# PATH export. If you added one, it's still there — this is the one manual step.
case ":$PATH:" in
*":$LARRY_BIN_DIR:"*)
say "reminder: you may have added 'export PATH=\"$LARRY_BIN_DIR:\$PATH\"' to your shell rc. Remove that line by hand if $LARRY_BIN_DIR is now empty." ;;
esac
say "uninstall complete."
[ "$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"