Guides

Paperclip Backup Retention and Maintenance

Paperclip creates hourly database backups (~125 MB each) with no built-in retention. Left unchecked, that's roughly 3 GB/day — it will fill your disk. This g...

Paperclip Backup Retention and Maintenance

Paperclip creates hourly database backups (~125 MB each) with no built-in retention. Left unchecked, that's roughly 3 GB/day — it will fill your disk. This guide covers tiered backup retention, scheduling, disk maintenance, and troubleshooting.

Backup Retention Script

The following script implements tiered retention: keeps the 24 most recent (hourly), 1 per day for 14 days, 1 per week for 4 weeks, and 1 per month forever.

Backups are stored in ~/.paperclip/instances/default/data/backups/.

cat > ~/prune-backups.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="$HOME/.paperclip/instances/default/data/backups"
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true

if [[ ! -d "$BACKUP_DIR" ]]; then
  echo "Backup directory not found: $BACKUP_DIR"
  exit 1
fi

now=$(date +%s)

declare -A keep_hourly   # 24 most recent
declare -A keep_daily    # 1 per day, last 14 days
declare -A keep_weekly   # 1 per week, last 4 weeks
declare -A keep_monthly  # 1 per month, forever

# Collect all backup files sorted newest first
mapfile -t files < <(ls -1t "$BACKUP_DIR"/*.db 2>/dev/null || true)

if [[ ${#files[@]} -eq 0 ]]; then
  echo "No backups found."
  exit 0
fi

# Keep 24 most recent unconditionally
for i in "${!files[@]}"; do
  if [[ $i -lt 24 ]]; then
    keep_hourly["${files[$i]}"]=1
  fi
done

for f in "${files[@]}"; do
  # Extract timestamp from file modification time
  if [[ "$(uname)" == "Darwin" ]]; then
    mtime=$(stat -f %m "$f")
  else
    mtime=$(stat -c %Y "$f")
  fi
  age_days=$(( (now - mtime) / 86400 ))

  day_key=$(date -d "@$mtime" +%Y-%m-%d 2>/dev/null || date -r "$mtime" +%Y-%m-%d)
  week_key=$(date -d "@$mtime" +%Y-%W 2>/dev/null || date -r "$mtime" +%Y-%W)
  month_key=$(date -d "@$mtime" +%Y-%m 2>/dev/null || date -r "$mtime" +%Y-%m)

  # Daily: keep first seen per day within 14 days
  if [[ $age_days -le 14 ]] && [[ -z "${keep_daily[$day_key]:-}" ]]; then
    keep_daily[$day_key]="$f"
  fi

  # Weekly: keep first seen per week within 28 days
  if [[ $age_days -le 28 ]] && [[ -z "${keep_weekly[$week_key]:-}" ]]; then
    keep_weekly[$week_key]="$f"
  fi

  # Monthly: keep first seen per month, forever
  if [[ -z "${keep_monthly[$month_key]:-}" ]]; then
    keep_monthly[$month_key]="$f"
  fi
done

# Build set of all files to keep
declare -A keep_set
for f in "${!keep_hourly[@]}"; do keep_set["$f"]=1; done
for key in "${!keep_daily[@]}"; do keep_set["${keep_daily[$key]}"]=1; done
for key in "${!keep_weekly[@]}"; do keep_set["${keep_weekly[$key]}"]=1; done
for key in "${!keep_monthly[@]}"; do keep_set["${keep_monthly[$key]}"]=1; done

# Delete everything else
deleted=0
kept=0
for f in "${files[@]}"; do
  if [[ -z "${keep_set[$f]:-}" ]]; then
    if $DRY_RUN; then
      echo "[dry-run] would delete: $(basename "$f")"
    else
      rm "$f"
    fi
    ((deleted++))
  else
    ((kept++))
  fi
done

echo "$(date): kept=$kept deleted=$deleted total=${#files[@]}${DRY_RUN:+ (dry run)}"
SCRIPT

chmod +x ~/prune-backups.sh

Preview first, then run for real:

~/prune-backups.sh --dry-run
~/prune-backups.sh

Scheduling the Script

Linux (EC2) — cron

Amazon Linux 2023 doesn't include cron by default:

sudo dnf install -y cronie
sudo systemctl enable crond
sudo systemctl start crond
(crontab -l 2>/dev/null; echo '0 5 * * * /home/ec2-user/prune-backups.sh >> /home/ec2-user/prune-backups.log 2>&1') | crontab -

macOS — launchd

cat > ~/Library/LaunchAgents/com.paperclip.prune-backups.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.paperclip.prune-backups</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>-c</string>
    <string>$HOME/prune-backups.sh >> $HOME/prune-backups.log 2>&1</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key>
    <integer>5</integer>
    <key>Minute</key>
    <integer>0</integer>
  </dict>
</dict>
</plist>
EOF

launchctl load ~/Library/LaunchAgents/com.paperclip.prune-backups.plist

Maintenance

Clear npm Cache

npm's cache can accumulate several GB over time:

npm cache clean --force

Check Disk Usage

df -h
du -sh ~/.paperclip ~/.npm ~/.nvm ~/paperclip 2>/dev/null | sort -rh

Troubleshooting

Tailscale SSH Host Key Mismatch After Reboot

If SSH fails with "No ED25519 host key is known" after a reboot, clear the cached key:

# macOS
rm "$HOME/Library/Application Support/tailscale/ssh_known_hosts"

# Linux
rm ~/.ssh/known_hosts   # or remove just the offending line

systemd Service Gets "Command Not Found" Errors

The PATH in the unit file must include every tool directory. Check the key paths table below and make sure each one is in your Environment=PATH=... line.

Paperclip Won't Start After EBS Resize

If you resized the EBS volume but didn't extend the filesystem, you'll still see ENOSPC errors. Run growpart + xfs_growfs (see the EC2 setup guide).


Key Paths Reference

Use this table when building the PATH for systemd or launchd — every directory containing a tool Paperclip calls must be listed.

Tool Linux (EC2) macOS (Homebrew)
node ~/.nvm/versions/node/v24.14.0/bin/node ~/.nvm/versions/node/v24.14.0/bin/node
pnpm ~/.local/share/pnpm/pnpm ~/.local/share/pnpm/pnpm
claude ~/.local/bin/claude ~/.local/bin/claude
wrangler ~/.local/share/pnpm/wrangler ~/.local/share/pnpm/wrangler
gh /usr/bin/gh /opt/homebrew/bin/gh

Node version will vary — check with which node and adjust accordingly.

Ready to build?

Go from idea to launched product in a week with AI-assisted development.