|
#!/usr/bin/env bash |
|
# |
|
# cleanup.bash - Clean up temporary/ephemeral files from ~/.claude/ |
|
# |
|
# Safe to run at any time. Skips files from currently active sessions. |
|
# Does NOT delete: CLAUDE.md, settings.json, skills/, plugins/, history.jsonl, |
|
# session transcripts (*.jsonl), session indexes, teams/, etc. |
|
# |
|
# Usage: |
|
# bash cleanup.bash # dry run (default) |
|
# bash cleanup.bash --dry-run |
|
# bash cleanup.bash --run # actually delete files |
|
# |
|
|
|
set -euo pipefail |
|
|
|
CLAUDE_DIR="${HOME}/.claude" |
|
DRY_RUN=true |
|
TOTAL_FREED=0 |
|
|
|
if [[ "${1:-}" == "--run" ]]; then |
|
DRY_RUN=false |
|
elif [[ "${1:-}" != "--dry-run" && -n "${1:-}" ]]; then |
|
echo "Usage: $0 [--dry-run | --run]" |
|
echo " --dry-run Show what would be deleted (default)" |
|
echo " --run Actually delete files" |
|
exit 1 |
|
fi |
|
|
|
if ! [[ -d "$CLAUDE_DIR" ]]; then |
|
echo "Error: $CLAUDE_DIR does not exist" |
|
exit 1 |
|
fi |
|
|
|
# Collect active session IDs by checking which files are currently open |
|
ACTIVE_SESSIONS=() |
|
|
|
# Use lsof to find open files in the claude dir (works on macOS and Linux) |
|
if command -v lsof &>/dev/null; then |
|
while IFS= read -r uuid; do |
|
[[ -n "$uuid" ]] && ACTIVE_SESSIONS+=("$uuid") |
|
done < <(lsof +D "$CLAUDE_DIR/debug" 2>/dev/null \ |
|
| awk '{print $NF}' \ |
|
| grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' \ |
|
| sort -u || true) |
|
fi |
|
|
|
# Also treat very recently modified debug logs as active (within last 5 min) |
|
while IFS= read -r f; do |
|
[[ -e "$f" ]] || continue |
|
uuid=$(basename "$f" .txt) |
|
ACTIVE_SESSIONS+=("$uuid") |
|
done < <(find "$CLAUDE_DIR/debug" -name "*.txt" -mmin -5 2>/dev/null || true) |
|
|
|
# Deduplicate |
|
if [[ ${#ACTIVE_SESSIONS[@]} -gt 0 ]]; then |
|
readarray -t ACTIVE_SESSIONS < <(printf '%s\n' "${ACTIVE_SESSIONS[@]}" | sort -u) |
|
fi |
|
|
|
is_active_session() { |
|
local check_id="$1" |
|
for active_id in "${ACTIVE_SESSIONS[@]+"${ACTIVE_SESSIONS[@]}"}"; do |
|
if [[ "$check_id" == "$active_id" ]]; then |
|
return 0 |
|
fi |
|
done |
|
return 1 |
|
} |
|
|
|
# Get file/dir size in bytes (portable) |
|
get_size() { |
|
if [[ -e "$1" ]]; then |
|
if [[ -d "$1" ]]; then |
|
du -sk "$1" 2>/dev/null | awk '{print $1 * 1024}' |
|
else |
|
stat -f%z "$1" 2>/dev/null || stat --format=%s "$1" 2>/dev/null || echo 0 |
|
fi |
|
else |
|
echo 0 |
|
fi |
|
} |
|
|
|
human_size() { |
|
local bytes=$1 |
|
if (( bytes >= 1073741824 )); then |
|
printf "%.1f GB" "$(echo "scale=1; $bytes / 1073741824" | bc)" |
|
elif (( bytes >= 1048576 )); then |
|
printf "%.1f MB" "$(echo "scale=1; $bytes / 1048576" | bc)" |
|
elif (( bytes >= 1024 )); then |
|
printf "%.1f KB" "$(echo "scale=1; $bytes / 1024" | bc)" |
|
else |
|
printf "%d B" "$bytes" |
|
fi |
|
} |
|
|
|
remove_item() { |
|
local path="$1" |
|
local desc="$2" |
|
local size |
|
size=$(get_size "$path") |
|
TOTAL_FREED=$((TOTAL_FREED + size)) |
|
|
|
if $DRY_RUN; then |
|
echo " [DRY RUN] Would delete: $path ($(human_size "$size")) - $desc" |
|
else |
|
rm -rf "$path" |
|
echo " Deleted: $path ($(human_size "$size")) - $desc" |
|
fi |
|
} |
|
|
|
echo "=== Claude Code Cleanup ===" |
|
echo "Mode: $( $DRY_RUN && echo 'DRY RUN (use --run to actually delete)' || echo 'LIVE - deleting files' )" |
|
echo "Active sessions detected: ${#ACTIVE_SESSIONS[@]}" |
|
if [[ ${#ACTIVE_SESSIONS[@]} -gt 0 ]]; then |
|
echo " Skipping files for: ${ACTIVE_SESSIONS[*]}" |
|
fi |
|
echo "" |
|
|
|
# --- 1. Security warning state files --- |
|
echo "--- Security warning state files ---" |
|
for f in "$CLAUDE_DIR"/security_warnings_state_*.json; do |
|
[[ -e "$f" ]] || continue |
|
uuid=$(basename "$f" | sed 's/security_warnings_state_//;s/\.json//') |
|
if is_active_session "$uuid"; then |
|
echo " [SKIP] $f (active session)" |
|
continue |
|
fi |
|
remove_item "$f" "session security warnings" |
|
done |
|
|
|
# --- 2. Debug logs --- |
|
echo "" |
|
echo "--- Debug logs ---" |
|
count=0 |
|
for f in "$CLAUDE_DIR"/debug/*.txt; do |
|
[[ -e "$f" ]] || continue |
|
uuid=$(basename "$f" .txt) |
|
if is_active_session "$uuid"; then |
|
echo " [SKIP] $f (active session)" |
|
continue |
|
fi |
|
remove_item "$f" "debug log" |
|
count=$((count + 1)) |
|
done |
|
echo " ($count debug log files)" |
|
|
|
# --- 3. Shell snapshots --- |
|
echo "" |
|
echo "--- Shell snapshots ---" |
|
count=0 |
|
for f in "$CLAUDE_DIR"/shell-snapshots/snapshot-*.sh; do |
|
[[ -e "$f" ]] || continue |
|
remove_item "$f" "shell snapshot" |
|
count=$((count + 1)) |
|
done |
|
echo " ($count shell snapshot files)" |
|
|
|
# --- 4. Paste cache --- |
|
echo "" |
|
echo "--- Paste cache ---" |
|
count=0 |
|
for f in "$CLAUDE_DIR"/paste-cache/*.txt; do |
|
[[ -e "$f" ]] || continue |
|
remove_item "$f" "paste cache" |
|
count=$((count + 1)) |
|
done |
|
echo " ($count paste cache files)" |
|
|
|
# --- 5. Empty session-env directories --- |
|
echo "" |
|
echo "--- Session env directories ---" |
|
count=0 |
|
for d in "$CLAUDE_DIR"/session-env/*/; do |
|
[[ -d "$d" ]] || continue |
|
uuid=$(basename "$d") |
|
if is_active_session "$uuid"; then |
|
echo " [SKIP] $d (active session)" |
|
continue |
|
fi |
|
remove_item "$d" "session env dir" |
|
count=$((count + 1)) |
|
done |
|
echo " ($count session-env directories)" |
|
|
|
# --- 6. Todo lists --- |
|
echo "" |
|
echo "--- Todo lists ---" |
|
count=0 |
|
for f in "$CLAUDE_DIR"/todos/*.json; do |
|
[[ -e "$f" ]] || continue |
|
uuid=$(echo "$(basename "$f")" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1) |
|
if [[ -n "$uuid" ]] && is_active_session "$uuid"; then |
|
echo " [SKIP] $f (active session)" |
|
continue |
|
fi |
|
remove_item "$f" "todo list" |
|
count=$((count + 1)) |
|
done |
|
echo " ($count todo files)" |
|
|
|
# --- 7. Task tracking --- |
|
echo "" |
|
echo "--- Task tracking ---" |
|
count=0 |
|
for d in "$CLAUDE_DIR"/tasks/*/; do |
|
[[ -d "$d" ]] || continue |
|
uuid=$(basename "$d") |
|
if is_active_session "$uuid"; then |
|
echo " [SKIP] $d (active session)" |
|
continue |
|
fi |
|
remove_item "$d" "task data" |
|
count=$((count + 1)) |
|
done |
|
echo " ($count task directories)" |
|
|
|
# --- 8. Project subagent logs and tool-results --- |
|
echo "" |
|
echo "--- Project subagent logs & tool results ---" |
|
sa_count=0 |
|
tr_count=0 |
|
for project_dir in "$CLAUDE_DIR"/projects/*/; do |
|
[[ -d "$project_dir" ]] || continue |
|
for session_dir in "$project_dir"/*/; do |
|
[[ -d "$session_dir" ]] || continue |
|
# Skip if it's not a UUID directory (e.g., sessions-index.json) |
|
uuid=$(basename "$session_dir") |
|
echo "$uuid" | grep -qE '^[0-9a-f]{8}-' || continue |
|
if is_active_session "$uuid"; then |
|
echo " [SKIP] $session_dir (active session)" |
|
continue |
|
fi |
|
if [[ -d "$session_dir/subagents" ]]; then |
|
for f in "$session_dir"/subagents/*.jsonl; do |
|
[[ -e "$f" ]] || continue |
|
remove_item "$f" "subagent log" |
|
sa_count=$((sa_count + 1)) |
|
done |
|
fi |
|
if [[ -d "$session_dir/tool-results" ]]; then |
|
for f in "$session_dir"/tool-results/*.txt; do |
|
[[ -e "$f" ]] || continue |
|
remove_item "$f" "tool result cache" |
|
tr_count=$((tr_count + 1)) |
|
done |
|
fi |
|
done |
|
done |
|
echo " ($sa_count subagent logs, $tr_count tool result files)" |
|
|
|
# --- 9. Failed telemetry events --- |
|
echo "" |
|
echo "--- Failed telemetry events ---" |
|
count=0 |
|
for f in "$CLAUDE_DIR"/telemetry/*.json; do |
|
[[ -e "$f" ]] || continue |
|
remove_item "$f" "failed telemetry" |
|
count=$((count + 1)) |
|
done |
|
echo " ($count telemetry files)" |
|
|
|
# --- 10. Stats cache --- |
|
echo "" |
|
echo "--- Stats cache ---" |
|
if [[ -f "$CLAUDE_DIR/stats-cache.json" ]]; then |
|
remove_item "$CLAUDE_DIR/stats-cache.json" "stats cache" |
|
fi |
|
|
|
# --- 11. Orphaned plugin cache versions --- |
|
echo "" |
|
echo "--- Orphaned plugin cache versions ---" |
|
count=0 |
|
while IFS= read -r orphan_marker; do |
|
[[ -e "$orphan_marker" ]] || continue |
|
ver_dir=$(dirname "$orphan_marker") |
|
remove_item "$ver_dir" "orphaned plugin $(basename "$(dirname "$ver_dir")")/$(basename "$ver_dir")" |
|
count=$((count + 1)) |
|
done < <(find "$CLAUDE_DIR/plugins/cache" -name ".orphaned_at" 2>/dev/null || true) |
|
echo " ($count orphaned plugin versions)" |
|
|
|
# --- Summary --- |
|
echo "" |
|
echo "=== Summary ===" |
|
echo "Total space $( $DRY_RUN && echo 'that would be freed' || echo 'freed' ): $(human_size $TOTAL_FREED)" |
|
echo "" |
|
if $DRY_RUN; then |
|
echo "Run with --run to actually delete these files." |
|
fi |