Skip to content

Instantly share code, notes, and snippets.

@swateek
Last active April 10, 2026 05:01
Show Gist options
  • Select an option

  • Save swateek/9a9c89fae5900ce1c31e596a8fe4a8cb to your computer and use it in GitHub Desktop.

Select an option

Save swateek/9a9c89fae5900ce1c31e596a8fe4a8cb to your computer and use it in GitHub Desktop.
Vagrant Cleanup Script
#!/bin/bash
CYAN='\033[0;36m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m'
# Use $HOME by default, or override with CLEANUP_HOME env var
# e.g. CLEANUP_HOME=/home/otheruser ~/cleanup.sh
CLEANUP_HOME="${CLEANUP_HOME:-$HOME}"
TOTAL_FREED=0
AUTO_CONFIRM=false
print_header() {
echo ""
echo -e "${BOLD}${CYAN}========================================${NC}"
echo -e "${BOLD}${CYAN} $1${NC}"
echo -e "${BOLD}${CYAN}========================================${NC}"
}
print_size() {
du -sh "$1" 2>/dev/null | cut -f1
}
bytes_to_human() {
numfmt --to=iec "$1" 2>/dev/null || echo "${1}B"
}
get_dir_bytes() {
du -sb "$1" 2>/dev/null | cut -f1
}
confirm() {
if $AUTO_CONFIRM; then
return 0
fi
read -rp "⚠️ $1 (yes/no): " CONFIRM
[[ "$CONFIRM" == "yes" ]]
}
freed_message() {
echo -e "${GREEN}βœ… Freed: $1${NC}"
}
skip_message() {
echo -e "${YELLOW}⏭️ Skipped.${NC}"
}
# ─────────────────────────────────────────
# 1. Terraform .terraform directories
# ─────────────────────────────────────────
cleanup_terraform() {
print_header "Terraform Provider Cache (.terraform)"
mapfile -t DIRS < <(find "$CLEANUP_HOME" -type d -name ".terraform" 2>/dev/null)
if [ ${#DIRS[@]} -eq 0 ]; then
echo " No .terraform directories found."
return
fi
echo ""
for i in "${!DIRS[@]}"; do
SIZE=$(print_size "${DIRS[$i]}")
echo " [$i] $SIZE ${DIRS[$i]}"
done
if $AUTO_CONFIRM; then
CHOICE="a"
else
echo ""
echo " Options: (a)ll (s)elect indexes (n)one"
read -rp " Choice: " CHOICE
fi
case "$CHOICE" in
a)
BEFORE=0
for DIR in "${DIRS[@]}"; do
BEFORE=$((BEFORE + $(get_dir_bytes "$DIR")))
rm -rf "$DIR"
echo " πŸ—‘οΈ Deleted: $DIR"
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
;;
s)
read -rp " Enter indexes (e.g. 0,2,4): " INDEXES
IFS=',' read -ra SELECTED <<< "$INDEXES"
BEFORE=0
for i in "${SELECTED[@]}"; do
i=$(echo "$i" | tr -d ' ')
if [[ -n "${DIRS[$i]}" ]]; then
BEFORE=$((BEFORE + $(get_dir_bytes "${DIRS[$i]}")))
rm -rf "${DIRS[$i]}"
echo " πŸ—‘οΈ Deleted: ${DIRS[$i]}"
else
echo " ⚠️ Invalid index: $i β€” skipping"
fi
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
;;
*)
skip_message
;;
esac
}
# ─────────────────────────────────────────
# 2. Playwright browser cache
# ─────────────────────────────────────────
cleanup_playwright() {
print_header "Playwright Browser Cache"
PLAYWRIGHT_DIRS=(
"$CLEANUP_HOME/.cache/ms-playwright"
"$CLEANUP_HOME/.cache/ms-playwright-go"
)
FOUND=0
for DIR in "${PLAYWRIGHT_DIRS[@]}"; do
if [ -d "$DIR" ]; then
SIZE=$(print_size "$DIR")
echo " πŸ“ $SIZE $DIR"
FOUND=1
fi
done
if [ $FOUND -eq 0 ]; then
echo " No Playwright cache found."
return
fi
echo ""
if confirm "Delete Playwright browser cache?"; then
BEFORE=0
for DIR in "${PLAYWRIGHT_DIRS[@]}"; do
if [ -d "$DIR" ]; then
BEFORE=$((BEFORE + $(get_dir_bytes "$DIR")))
rm -rf "$DIR"
echo " πŸ—‘οΈ Deleted: $DIR"
fi
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 3. APT package cache
# ─────────────────────────────────────────
cleanup_apt() {
print_header "APT Package Cache"
SIZE=$(print_size "/var/cache/apt")
echo " πŸ“ $SIZE /var/cache/apt"
echo ""
if confirm "Run apt clean, autoclean, and autoremove?"; then
BEFORE=$(get_dir_bytes "/var/cache/apt")
sudo apt-get clean -y
sudo apt-get autoclean -y
sudo apt-get autoremove -y --purge
AFTER=$(get_dir_bytes "/var/cache/apt")
DIFF=$((BEFORE - AFTER))
TOTAL_FREED=$((TOTAL_FREED + DIFF))
freed_message "$(bytes_to_human $DIFF)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 4. Journal logs
# ─────────────────────────────────────────
cleanup_journal() {
print_header "Systemd Journal Logs"
SIZE=$(journalctl --disk-usage 2>/dev/null | grep -oP '[\d.]+\s*\w+(?= on disk)')
echo " πŸ“ Journal logs currently using: ${SIZE:-unknown}"
echo ""
if confirm "Truncate journal logs to last 7 days / 50MB max?"; then
sudo journalctl --vacuum-time=7d
sudo journalctl --vacuum-size=50M
freed_message "Journal logs truncated"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 5. Temp files
# ─────────────────────────────────────────
cleanup_tmp() {
print_header "Temp Files (/tmp, /var/tmp)"
TMP_SIZE=$(print_size "/tmp")
VAR_TMP_SIZE=$(print_size "/var/tmp")
echo " πŸ“ $TMP_SIZE /tmp"
echo " πŸ“ $VAR_TMP_SIZE /var/tmp"
echo ""
if confirm "Clear /tmp and /var/tmp?"; then
BEFORE=$(($(get_dir_bytes "/tmp") + $(get_dir_bytes "/var/tmp")))
sudo rm -rf /tmp/* /var/tmp/*
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 6. Old log files
# ─────────────────────────────────────────
cleanup_logs() {
print_header "Old Log Files (/var/log)"
SIZE=$(print_size "/var/log")
echo " πŸ“ $SIZE /var/log (compressed/rotated logs)"
echo ""
if confirm "Remove compressed and rotated log files (*.gz, *.old)?"; then
BEFORE=$(find /var/log -type f \( -name "*.gz" -o -name "*.old" \) -exec du -sb {} + 2>/dev/null | awk '{sum+=$1} END{print sum+0}')
sudo find /var/log -type f \( -name "*.gz" -o -name "*.old" \) -delete
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 7. NPM cache
# ─────────────────────────────────────────
cleanup_npm() {
print_header "NPM Cache"
NPM_CACHE="$CLEANUP_HOME/.npm"
if [ ! -d "$NPM_CACHE" ]; then
echo " No NPM cache found."
return
fi
SIZE=$(print_size "$NPM_CACHE")
echo " πŸ“ $SIZE $NPM_CACHE"
echo ""
if confirm "Clear NPM cache?"; then
BEFORE=$(get_dir_bytes "$NPM_CACHE")
npm cache clean --force 2>/dev/null
AFTER=$(get_dir_bytes "$NPM_CACHE")
DIFF=$((BEFORE - AFTER))
TOTAL_FREED=$((TOTAL_FREED + DIFF))
freed_message "$(bytes_to_human $DIFF)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 8. NVM old Node versions
# ─────────────────────────────────────────
cleanup_nvm() {
print_header "NVM β€” Old Node Versions"
NVM_DIR="$CLEANUP_HOME/.nvm/versions/node"
if [ ! -d "$NVM_DIR" ]; then
echo " NVM not found."
return
fi
CURRENT=$(node --version 2>/dev/null)
echo " Current active Node version: ${CURRENT:-unknown}"
echo ""
mapfile -t VERSIONS < <(ls "$NVM_DIR")
if [ ${#VERSIONS[@]} -le 1 ]; then
echo " Only one Node version installed, nothing to prune."
return
fi
echo " Installed versions:"
for i in "${!VERSIONS[@]}"; do
SIZE=$(print_size "$NVM_DIR/${VERSIONS[$i]}")
ACTIVE=""
[[ "v${VERSIONS[$i]#v}" == "$CURRENT" ]] && ACTIVE=" ← current"
echo " [$i] $SIZE ${VERSIONS[$i]}$ACTIVE"
done
echo ""
if $AUTO_CONFIRM; then
# In auto mode, delete all non-current versions
BEFORE=0
for i in "${!VERSIONS[@]}"; do
VER_PATH="$NVM_DIR/${VERSIONS[$i]}"
if [[ "v${VERSIONS[$i]#v}" == "$CURRENT" ]]; then
echo " ⏭️ Keeping current: ${VERSIONS[$i]}"
else
BEFORE=$((BEFORE + $(get_dir_bytes "$VER_PATH")))
rm -rf "$VER_PATH"
echo " πŸ—‘οΈ Deleted: ${VERSIONS[$i]}"
fi
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
echo " Options: (s)elect indexes to delete (n)one"
read -rp " Choice: " CHOICE
case "$CHOICE" in
s)
read -rp " Enter indexes to delete (e.g. 0,2): " INDEXES
IFS=',' read -ra SELECTED <<< "$INDEXES"
BEFORE=0
for i in "${SELECTED[@]}"; do
i=$(echo "$i" | tr -d ' ')
if [[ -n "${VERSIONS[$i]}" ]]; then
VER_PATH="$NVM_DIR/${VERSIONS[$i]}"
if [[ "v${VERSIONS[$i]#v}" == "$CURRENT" ]]; then
echo " ⚠️ Skipping current version: ${VERSIONS[$i]}"
else
BEFORE=$((BEFORE + $(get_dir_bytes "$VER_PATH")))
rm -rf "$VER_PATH"
echo " πŸ—‘οΈ Deleted: ${VERSIONS[$i]}"
fi
else
echo " ⚠️ Invalid index: $i β€” skipping"
fi
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
;;
*)
skip_message
;;
esac
fi
}
# ─────────────────────────────────────────
# 9. Zsh completion dumps
# ─────────────────────────────────────────
cleanup_zcompdump() {
print_header "Zsh Completion Dumps (.zcompdump*)"
mapfile -t FILES < <(find "$CLEANUP_HOME" -maxdepth 1 -name ".zcompdump*" 2>/dev/null)
if [ ${#FILES[@]} -eq 0 ]; then
echo " No .zcompdump files found."
return
fi
BEFORE=0
for f in "${FILES[@]}"; do
SIZE=$(print_size "$f")
BYTES=$(get_dir_bytes "$f")
BEFORE=$((BEFORE + BYTES))
echo " πŸ“„ $SIZE $f"
done
echo ""
if confirm "Delete all .zcompdump files? (they regenerate automatically)"; then
for f in "${FILES[@]}"; do
rm -f "$f"
echo " πŸ—‘οΈ Deleted: $f"
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 10. Cursor server old versions
# ─────────────────────────────────────────
cleanup_cursor_server() {
print_header "Cursor Server β€” Old Versions"
CURSOR_SERVER_DIR="$CLEANUP_HOME/.cursor-server"
if [ ! -d "$CURSOR_SERVER_DIR" ]; then
echo " No .cursor-server directory found."
return
fi
mapfile -t VERSIONS < <(ls "$CURSOR_SERVER_DIR" 2>/dev/null)
if [ ${#VERSIONS[@]} -le 1 ]; then
echo " Only one Cursor server version installed, nothing to prune."
return
fi
echo " Installed versions (newest last):"
for i in "${!VERSIONS[@]}"; do
SIZE=$(print_size "$CURSOR_SERVER_DIR/${VERSIONS[$i]}")
echo " [$i] $SIZE ${VERSIONS[$i]}"
done
LATEST_IDX=$(( ${#VERSIONS[@]} - 1 ))
echo ""
if $AUTO_CONFIRM; then
# Keep only the latest version
BEFORE=0
for i in "${!VERSIONS[@]}"; do
if [ "$i" -eq "$LATEST_IDX" ]; then
echo " ⏭️ Keeping latest: ${VERSIONS[$i]}"
else
VER_PATH="$CURSOR_SERVER_DIR/${VERSIONS[$i]}"
BEFORE=$((BEFORE + $(get_dir_bytes "$VER_PATH")))
rm -rf "$VER_PATH"
echo " πŸ—‘οΈ Deleted: ${VERSIONS[$i]}"
fi
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
echo " Tip: Keep the highest-numbered version, delete the rest."
echo " Options: (s)elect indexes to delete (n)one"
read -rp " Choice: " CHOICE
case "$CHOICE" in
s)
read -rp " Enter indexes to delete (e.g. 0,1): " INDEXES
IFS=',' read -ra SELECTED <<< "$INDEXES"
BEFORE=0
for i in "${SELECTED[@]}"; do
i=$(echo "$i" | tr -d ' ')
if [[ -n "${VERSIONS[$i]}" ]]; then
VER_PATH="$CURSOR_SERVER_DIR/${VERSIONS[$i]}"
BEFORE=$((BEFORE + $(get_dir_bytes "$VER_PATH")))
rm -rf "$VER_PATH"
echo " πŸ—‘οΈ Deleted: ${VERSIONS[$i]}"
else
echo " ⚠️ Invalid index: $i β€” skipping"
fi
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
;;
*)
skip_message
;;
esac
fi
}
# ─────────────────────────────────────────
# 11. VS Code Remote Server β€” Old Versions
# ─────────────────────────────────────────
# ~/.vscode-server/cli/servers/ holds one subdirectory per VS Code
# server version (named by commit hash). Deleting the ACTIVE version
# breaks remote SSH connections until VS Code re-downloads it.
# We always keep the most recently modified version (= the one VS Code
# last connected with) and offer to delete the rest.
# ─────────────────────────────────────────
cleanup_vscode_server() {
print_header "VS Code Remote Server β€” Old Versions (~/.vscode-server)"
# VS Code stores servers under either of these paths depending on version
VSCODE_SERVER_ROOTS=(
"$CLEANUP_HOME/.vscode-server/cli/servers"
"$CLEANUP_HOME/.vscode-server/bin"
)
FOUND=0
for ROOT in "${VSCODE_SERVER_ROOTS[@]}"; do
[ -d "$ROOT" ] && FOUND=1 && break
done
if [ $FOUND -eq 0 ]; then
echo " No VS Code remote server directory found."
return
fi
declare -a ALL_VERSIONS
declare -a ALL_VERSION_ROOTS
for ROOT in "${VSCODE_SERVER_ROOTS[@]}"; do
[ ! -d "$ROOT" ] && continue
while IFS= read -r -d '' DIR; do
ALL_VERSIONS+=("$DIR")
ALL_VERSION_ROOTS+=("$ROOT")
done < <(find "$ROOT" -maxdepth 1 -mindepth 1 -type d -print0 2>/dev/null)
done
TOTAL=${#ALL_VERSIONS[@]}
if [ "$TOTAL" -eq 0 ]; then
echo " No server versions found."
return
fi
if [ "$TOTAL" -le 1 ]; then
echo " Only one VS Code server version installed β€” nothing to prune."
SIZE=$(print_size "${ALL_VERSIONS[0]}")
echo " βœ… $SIZE $(basename "${ALL_VERSIONS[0]}") ← keeping"
return
fi
# Sort by modification time ascending (oldest first), keep the newest
mapfile -t SORTED < <(
for DIR in "${ALL_VERSIONS[@]}"; do
echo "$(stat -c '%Y' "$DIR" 2>/dev/null) $DIR"
done | sort -n | awk '{print $2}'
)
LATEST="${SORTED[-1]}"
LATEST_NAME=$(basename "$LATEST")
echo " Installed versions (oldest β†’ newest):"
DELETABLE_BYTES=0
declare -a TO_DELETE
for i in "${!SORTED[@]}"; do
DIR="${SORTED[$i]}"
NAME=$(basename "$DIR")
SIZE=$(print_size "$DIR")
if [[ "$DIR" == "$LATEST" ]]; then
echo " [$i] $SIZE $NAME ← ACTIVE (will keep)"
else
BYTES=$(get_dir_bytes "$DIR")
DELETABLE_BYTES=$((DELETABLE_BYTES + BYTES))
TO_DELETE+=("$DIR")
echo " [$i] $SIZE $NAME"
fi
done
DELETE_COUNT=${#TO_DELETE[@]}
echo ""
echo " Candidates to delete: $DELETE_COUNT old version(s) (~$(bytes_to_human $DELETABLE_BYTES))"
echo " ⚠️ The ACTIVE version ($LATEST_NAME) is always preserved."
echo ""
if [ $DELETE_COUNT -eq 0 ]; then
echo " Nothing to prune."
return
fi
if $AUTO_CONFIRM || confirm "Delete old VS Code server versions (active version is safe)?"; then
FREED=0
for DIR in "${TO_DELETE[@]}"; do
FREED=$((FREED + $(get_dir_bytes "$DIR")))
rm -rf "$DIR"
echo " πŸ—‘οΈ Deleted: $(basename "$DIR")"
done
TOTAL_FREED=$((TOTAL_FREED + FREED))
freed_message "$(bytes_to_human $FREED)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 12. Virtualenv cache
# ─────────────────────────────────────────
cleanup_virtualenv() {
print_header "Virtualenv Cache"
VENV_CACHE="$CLEANUP_HOME/.local/share/virtualenv"
if [ ! -d "$VENV_CACHE" ]; then
echo " No virtualenv cache found."
return
fi
SIZE=$(print_size "$VENV_CACHE")
echo " πŸ“ $SIZE $VENV_CACHE"
echo ""
if confirm "Clear virtualenv cache?"; then
BEFORE=$(get_dir_bytes "$VENV_CACHE")
rm -rf "$VENV_CACHE"
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 12. General .cache sweep
# ─────────────────────────────────────────
cleanup_cache() {
print_header "General Cache (~/.cache)"
CACHE_DIR="$CLEANUP_HOME/.cache"
if [ ! -d "$CACHE_DIR" ]; then
echo " No .cache directory found."
return
fi
echo " Cache subdirectories:"
mapfile -t SUBDIRS < <(find "$CACHE_DIR" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort)
# Dirs inside ~/.cache that must never be auto-deleted:
# ms-playwright / ms-playwright-go β†’ handled by their own section
# vscode-server / vscode-server-insiders β†’ breaks VS Code Remote SSH if wiped
is_cache_protected() {
local NAME="$1"
local PROTECTED=("ms-playwright" "ms-playwright-go" "vscode-server" "vscode-server-insiders")
for P in "${PROTECTED[@]}"; do
[[ "$NAME" == "$P" ]] && return 0
done
return 1
}
TOTAL_CACHE=0
for i in "${!SUBDIRS[@]}"; do
NAME=$(basename "${SUBDIRS[$i]}")
is_cache_protected "$NAME" && continue
SIZE=$(print_size "${SUBDIRS[$i]}")
BYTES=$(get_dir_bytes "${SUBDIRS[$i]}")
TOTAL_CACHE=$((TOTAL_CACHE + BYTES))
echo " [$i] $SIZE $NAME"
done
echo ""
echo " Total: $(bytes_to_human $TOTAL_CACHE)"
echo " (vscode-server, ms-playwright excluded β€” managed by their own sections)"
echo ""
if $AUTO_CONFIRM; then
BEFORE=0
for DIR in "${SUBDIRS[@]}"; do
NAME=$(basename "$DIR")
is_cache_protected "$NAME" && continue
BEFORE=$((BEFORE + $(get_dir_bytes "$DIR")))
rm -rf "$DIR"
echo " πŸ—‘οΈ Deleted: $NAME"
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
echo " Options: (a)ll (s)elect indexes (n)one"
read -rp " Choice: " CHOICE
case "$CHOICE" in
a)
if confirm "Delete ALL .cache subdirectories (except protected dirs)?"; then
BEFORE=0
for DIR in "${SUBDIRS[@]}"; do
NAME=$(basename "$DIR")
is_cache_protected "$NAME" && continue
BEFORE=$((BEFORE + $(get_dir_bytes "$DIR")))
rm -rf "$DIR"
echo " πŸ—‘οΈ Deleted: $NAME"
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
skip_message
fi
;;
s)
read -rp " Enter indexes (e.g. 0,2,4): " INDEXES
IFS=',' read -ra SELECTED <<< "$INDEXES"
BEFORE=0
for i in "${SELECTED[@]}"; do
i=$(echo "$i" | tr -d ' ')
if [[ -n "${SUBDIRS[$i]}" ]]; then
NAME=$(basename "${SUBDIRS[$i]}")
if is_cache_protected "$NAME"; then
echo " ⚠️ Skipping protected dir: $NAME"
continue
fi
BEFORE=$((BEFORE + $(get_dir_bytes "${SUBDIRS[$i]}")))
rm -rf "${SUBDIRS[$i]}"
echo " πŸ—‘οΈ Deleted: $NAME"
else
echo " ⚠️ Invalid index: $i β€” skipping"
fi
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
;;
*)
skip_message
;;
esac
fi
}
# ─────────────────────────────────────────
# 13. Mozilla Firefox cache
# ─────────────────────────────────────────
cleanup_mozilla() {
print_header "Mozilla Firefox Cache"
FIREFOX_DIR="$CLEANUP_HOME/.mozilla/firefox"
if [ ! -d "$FIREFOX_DIR" ]; then
echo " No Firefox profile found."
return
fi
mapfile -t CACHE_DIRS < <(find "$FIREFOX_DIR" -maxdepth 2 -type d \( -name "cache2" -o -name "startupCache" -o -name "jumpListCache" \) 2>/dev/null)
if [ ${#CACHE_DIRS[@]} -eq 0 ]; then
echo " No Firefox cache directories found."
return
fi
BEFORE=0
for DIR in "${CACHE_DIRS[@]}"; do
SIZE=$(print_size "$DIR")
BYTES=$(get_dir_bytes "$DIR")
BEFORE=$((BEFORE + BYTES))
echo " πŸ“ $SIZE $DIR"
done
echo ""
if confirm "Clear Firefox cache directories? (profile/bookmarks/passwords are untouched)"; then
for DIR in "${CACHE_DIRS[@]}"; do
rm -rf "$DIR"
echo " πŸ—‘οΈ Deleted: $DIR"
done
TOTAL_FREED=$((TOTAL_FREED + BEFORE))
freed_message "$(bytes_to_human $BEFORE)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 14. Claude Code β€” Old Session Transcripts
# ─────────────────────────────────────────
# ~/.claude/projects/<project-slug>/<session>.jsonl
# Each .jsonl is a full session transcript. Sessions grow quickly
# and can reach several GBs. We keep the N most recent per project
# and delete the rest. settings.json, CLAUDE.md, commands/, and
# .credentials.json are never touched.
# ─────────────────────────────────────────
cleanup_claude_sessions() {
print_header "Claude Code β€” Session Transcripts (~/.claude/projects)"
CLAUDE_PROJECTS_DIR="$CLEANUP_HOME/.claude/projects"
if [ ! -d "$CLAUDE_PROJECTS_DIR" ]; then
echo " No Claude Code projects directory found."
return
fi
# Default: keep the 3 most recent sessions per project
KEEP_COUNT=3
if ! $AUTO_CONFIRM; then
echo " How many recent sessions to keep per project? (default: 3)"
read -rp " Keep count [3]: " USER_KEEP
[[ "$USER_KEEP" =~ ^[0-9]+$ ]] && KEEP_COUNT="$USER_KEEP"
fi
mapfile -t PROJECT_DIRS < <(find "$CLAUDE_PROJECTS_DIR" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort)
if [ ${#PROJECT_DIRS[@]} -eq 0 ]; then
echo " No project session directories found."
return
fi
TOTAL_SESSIONS=0
DELETABLE_BYTES=0
echo ""
echo " Projects found (sessions older than the $KEEP_COUNT most recent will be pruned):"
echo ""
declare -A PROJECT_DELETE_MAP
for PROJ_DIR in "${PROJECT_DIRS[@]}"; do
PROJ_NAME=$(basename "$PROJ_DIR")
mapfile -t ALL_SESSIONS < <(find "$PROJ_DIR" -maxdepth 1 -name "*.jsonl" -type f 2>/dev/null | sort)
TOTAL=${#ALL_SESSIONS[@]}
KEEP=$KEEP_COUNT
[[ $KEEP -gt $TOTAL ]] && KEEP=$TOTAL
DELETE_COUNT=$((TOTAL - KEEP))
if [ $DELETE_COUNT -le 0 ]; then
echo " βœ… $PROJ_NAME ($TOTAL sessions β€” all within keep limit)"
PROJECT_DELETE_MAP["$PROJ_DIR"]=""
continue
fi
# Sessions to delete = oldest ones (sorted ascending, so head)
mapfile -t TO_DELETE < <(printf '%s\n' "${ALL_SESSIONS[@]}" | head -n "$DELETE_COUNT")
PROJ_BYTES=0
for f in "${TO_DELETE[@]}"; do
B=$(get_dir_bytes "$f")
PROJ_BYTES=$((PROJ_BYTES + B))
done
DELETABLE_BYTES=$((DELETABLE_BYTES + PROJ_BYTES))
TOTAL_SESSIONS=$((TOTAL_SESSIONS + DELETE_COUNT))
echo " πŸ—‚οΈ $PROJ_NAME"
echo " Sessions: $TOTAL total | keeping: $KEEP | deleting: $DELETE_COUNT | $(bytes_to_human $PROJ_BYTES)"
PROJECT_DELETE_MAP["$PROJ_DIR"]="${TO_DELETE[*]}"
done
echo ""
echo " Total to delete: $TOTAL_SESSIONS sessions (~$(bytes_to_human $DELETABLE_BYTES))"
echo ""
if [ $TOTAL_SESSIONS -eq 0 ]; then
echo " Nothing to prune β€” all projects are within the keep limit."
return
fi
if $AUTO_CONFIRM || confirm "Delete old Claude Code session transcripts (settings/credentials/CLAUDE.md untouched)?"; then
FREED=0
for PROJ_DIR in "${!PROJECT_DELETE_MAP[@]}"; do
FILES_STR="${PROJECT_DELETE_MAP[$PROJ_DIR]}"
[[ -z "$FILES_STR" ]] && continue
read -ra FILES <<< "$FILES_STR"
for f in "${FILES[@]}"; do
FREED=$((FREED + $(get_dir_bytes "$f")))
rm -f "$f"
echo " πŸ—‘οΈ Deleted: $(basename "$PROJ_DIR")/$(basename "$f")"
done
done
TOTAL_FREED=$((TOTAL_FREED + FREED))
freed_message "$(bytes_to_human $FREED)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# 15. Claude Code β€” Statsig Analytics Cache
# ─────────────────────────────────────────
# ~/.claude/statsig/ is a local analytics/feature-flag cache
# written by Claude Code. It is fully safe to delete β€” Claude Code
# rebuilds it on next launch. It never contains credentials or
# session history.
# ─────────────────────────────────────────
cleanup_claude_cache() {
print_header "Claude Code β€” Analytics & Misc Cache (~/.claude/statsig)"
CLAUDE_DIR="$CLEANUP_HOME/.claude"
if [ ! -d "$CLAUDE_DIR" ]; then
echo " No ~/.claude directory found (Claude Code not installed?)."
return
fi
# Dirs that are safe to nuke and regenerate automatically
SAFE_CACHE_DIRS=(
"$CLAUDE_DIR/statsig"
"$CLAUDE_DIR/shell-snapshots"
)
FOUND=0
TOTAL_BYTES=0
for DIR in "${SAFE_CACHE_DIRS[@]}"; do
if [ -d "$DIR" ]; then
SIZE=$(print_size "$DIR")
BYTES=$(get_dir_bytes "$DIR")
TOTAL_BYTES=$((TOTAL_BYTES + BYTES))
echo " πŸ“ $SIZE $DIR"
FOUND=1
fi
done
if [ $FOUND -eq 0 ]; then
echo " No Claude Code cache directories found."
return
fi
echo ""
echo " These are regenerated automatically on next Claude Code launch."
echo " ⚠️ settings.json / CLAUDE.md / .credentials.json are NOT touched."
echo ""
if $AUTO_CONFIRM || confirm "Delete Claude Code cache directories (statsig, shell-snapshots)?"; then
FREED=0
for DIR in "${SAFE_CACHE_DIRS[@]}"; do
if [ -d "$DIR" ]; then
FREED=$((FREED + $(get_dir_bytes "$DIR")))
rm -rf "$DIR"
echo " πŸ—‘οΈ Deleted: $DIR"
fi
done
TOTAL_FREED=$((TOTAL_FREED + FREED))
freed_message "$(bytes_to_human $FREED)"
else
skip_message
fi
}
# ─────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────
clear
echo -e "${BOLD}${CYAN}"
echo " β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— "
echo " β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—"
echo " β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•"
echo " β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β• "
echo " β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ "
echo " β•šβ•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β• "
echo -e "${NC}"
echo -e " ${BOLD}Vagrant Box Disk Cleanup Utility${NC}"
echo -e " Run this script regularly to keep your box clean."
echo -e " Home directory: ${CYAN}$CLEANUP_HOME${NC}"
echo ""
echo -e "${BOLD}Current Disk Usage:${NC}"
df -h / | tail -1 | awk '{printf " Used: %s / %s (%s full)\n", $3, $2, $5}'
echo ""
# ─── Single upfront prompt ───────────────
echo -e "${BOLD}${YELLOW}Run mode:${NC}"
echo " (y) Auto β€” clean everything without prompts (NVM/Cursor keep latest only)"
echo " (n) Interactive β€” confirm each section individually"
echo ""
read -rp " Auto-clean everything? (y/n): " RUN_MODE
echo ""
if [[ "$RUN_MODE" == "y" || "$RUN_MODE" == "yes" ]]; then
AUTO_CONFIRM=true
echo -e "${GREEN} Running in auto mode β€” cleaning all sections...${NC}"
else
AUTO_CONFIRM=false
echo -e "${CYAN} Running in interactive mode β€” you'll be asked per section.${NC}"
fi
echo ""
cleanup_terraform
cleanup_playwright
cleanup_npm
cleanup_nvm
cleanup_zcompdump
cleanup_cursor_server
cleanup_vscode_server
cleanup_virtualenv
cleanup_cache
cleanup_mozilla
cleanup_claude_sessions
cleanup_claude_cache
cleanup_apt
cleanup_journal
cleanup_tmp
cleanup_logs
# ─────────────────────────────────────────
# SUMMARY
# ─────────────────────────────────────────
print_header "Cleanup Summary"
echo -e " ${BOLD}${GREEN}Total space freed: $(bytes_to_human $TOTAL_FREED)${NC}"
echo ""
echo -e "${BOLD}Disk Usage After Cleanup:${NC}"
df -h / | tail -1 | awk '{printf " Used: %s / %s (%s full)\n", $3, $2, $5}'
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment