Skip to content

Instantly share code, notes, and snippets.

@PProvost
Created August 7, 2026 21:47
Show Gist options
  • Select an option

  • Save PProvost/b7eea8b66e63c52847f01b2aafe59263 to your computer and use it in GitHub Desktop.

Select an option

Save PProvost/b7eea8b66e63c52847f01b2aafe59263 to your computer and use it in GitHub Desktop.
Back up recently modified Claude Code session transcripts and their associated subagent session directories.
#!/usr/bin/env bash
#
# backup-claude-sessions.sh
#
# Back up recently modified Claude Code session transcripts and their
# associated subagent session directories.
#
# Claude Code project transcript directories can contain root-level session
# files such as:
#
# ff8d36ca-4de1-42de-9870-4a98cdd0db76.jsonl
#
# A session that used subagents may also have a corresponding directory:
#
# ff8d36ca-4de1-42de-9870-4a98cdd0db76/
#
# containing additional session artifacts. Some of those files may be older
# than the requested backup window even though the parent session itself was
# recently active.
#
# This script therefore performs two steps:
#
# 1. Recursively find and copy all files modified within the requested
# number of days, preserving their paths relative to SOURCE_DIR.
#
# 2. For every root-level .jsonl transcript selected by that time window,
# check for a same-named directory in SOURCE_DIR. If one exists, copy
# the ENTIRE directory regardless of the modification times of the files
# inside it. This ensures subagent transcripts belonging to the selected
# session are included.
#
# DAYS is interpreted as a rolling time window rather than calendar days.
# For example:
#
# DAYS=3 -> files modified within the last 72 hours
# DAYS=2 -> files modified within the last 48 hours
#
# Existing files in DEST_DIR may be updated, but this script does NOT delete
# files from the destination.
#
# Requirements:
#
# - bash
# - find
# - rsync
#
# Usage:
#
# backup-claude-sessions.sh [--dry-run] SOURCE_DIR DEST_DIR DAYS
#
# Arguments:
#
# SOURCE_DIR Claude Code project transcript directory
# DEST_DIR Directory where session files should be backed up
# DAYS Positive integer specifying the lookback window
#
# Options:
#
# --dry-run Show what would be copied without making changes
# -h, --help Show usage information
#
# Example:
#
# backup-claude-sessions.sh \
# ~/.claude/projects/-home-example-project \
# ~/backups/claude-sessions \
# 3
#
# Dry run:
#
# backup-claude-sessions.sh --dry-run \
# ~/.claude/projects/-home-example-project \
# ~/backups/claude-sessions \
# 3
#
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
backup-claude-sessions.sh [--dry-run] SOURCE_DIR DEST_DIR DAYS
Arguments:
SOURCE_DIR Claude Code project transcript directory
DEST_DIR Backup destination directory
DAYS Number of days to look back (rolling 24-hour periods)
Options:
--dry-run Show what would be copied without changing anything
-h, --help Show this help
Example:
backup-claude-sessions.sh \
~/.claude/projects/-home-example-project \
~/backups/claude-sessions \
3
Dry run:
backup-claude-sessions.sh --dry-run \
~/.claude/projects/-home-example-project \
~/backups/claude-sessions \
3
EOF
}
# ---------------------------------------------------------------------------
# Parse arguments
# ---------------------------------------------------------------------------
dry_run=false
positional=()
for arg in "$@"; do
case "$arg" in
--dry-run)
dry_run=true
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Error: unknown option: $arg" >&2
echo >&2
usage >&2
exit 1
;;
*)
positional+=("$arg")
;;
esac
done
if [[ ${#positional[@]} -ne 3 ]]; then
echo "Error: expected SOURCE_DIR, DEST_DIR, and DAYS." >&2
echo >&2
usage >&2
exit 1
fi
src="${positional[0]%/}"
dst="${positional[1]%/}"
days="${positional[2]}"
# ---------------------------------------------------------------------------
# Validate arguments
# ---------------------------------------------------------------------------
if [[ ! -d "$src" ]]; then
echo "Error: source directory does not exist: $src" >&2
exit 1
fi
if [[ ! "$days" =~ ^[0-9]+$ ]] || (( days < 1 )); then
echo "Error: DAYS must be a positive integer." >&2
exit 1
fi
minutes=$((days * 24 * 60))
# ---------------------------------------------------------------------------
# Configure rsync
# ---------------------------------------------------------------------------
rsync_opts=(-av)
if "$dry_run"; then
rsync_opts+=(-n)
echo "DRY RUN: no files will be changed."
else
mkdir -p "$dst"
fi
echo
echo "Source: $src"
echo "Destination: $dst"
echo "Lookback: $days day(s) / $minutes minutes"
echo
# ---------------------------------------------------------------------------
# Phase 1
#
# Copy every recently modified file below the project transcript directory.
# Paths are kept relative to SOURCE_DIR so nested artifacts retain their
# original directory structure.
# ---------------------------------------------------------------------------
echo "=== Copying recently modified files ==="
(
cd "$src"
find . -type f -mmin "-$minutes" -print0 |
rsync -0 "${rsync_opts[@]}" --files-from=- ./ "$dst/"
)
# ---------------------------------------------------------------------------
# Phase 2
#
# Identify root-level .jsonl transcripts that fall within the requested
# window. If SOURCE_DIR contains a same-named directory, copy that directory
# in full. Files inside it are intentionally NOT subject to the date filter.
#
# Example:
#
# abc123.jsonl -> abc123/
#
# This captures older subagent artifacts associated with a recently active
# parent session.
# ---------------------------------------------------------------------------
echo
echo "=== Copying complete subagent directories for selected sessions ==="
while IFS= read -r -d '' transcript; do
filename="${transcript#./}"
session_id="${filename%.jsonl}"
if [[ -d "$src/$session_id" ]]; then
echo
echo "Session has subagent directory: $session_id/"
rsync "${rsync_opts[@]}" \
"$src/$session_id/" \
"$dst/$session_id/"
fi
done < <(
cd "$src"
find . \
-maxdepth 1 \
-type f \
-name '*.jsonl' \
-mmin "-$minutes" \
-print0
)
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
echo
if "$dry_run"; then
echo "Dry run complete. No files were changed."
else
echo "Backup complete."
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment