Skip to content

Instantly share code, notes, and snippets.

@nemtsov
Last active May 2, 2026 12:01
Show Gist options
  • Select an option

  • Save nemtsov/8eaeaf04ae540d65e73a91ea61b294d9 to your computer and use it in GitHub Desktop.

Select an option

Save nemtsov/8eaeaf04ae540d65e73a91ea61b294d9 to your computer and use it in GitHub Desktop.
Move folder and maintain Claude history (original: curiouslychase/dotfiles/scripts/claude-mv; patched to move dotfiles)
#!/bin/bash
set -e
# Escape a string for safe use as a sed BRE pattern (delimiter |).
sed_escape_pattern() {
printf '%s' "$1" | sed 's/[][\\/.^$*|]/\\&/g'
}
# Escape a string for safe use as a sed replacement (delimiter |).
sed_escape_replacement() {
printf '%s' "$1" | sed 's/[\\&|/]/\\&/g'
}
# Usage: claude-mv <old_directory> <new_directory>
if [ $# -ne 2 ]; then
echo "Usage: claude-mv <old_directory> <new_directory>"
echo "Example: claude-mv ~/old-project ~/new-project"
exit 1
fi
OLD_DIR="$1"
NEW_DIR="$2"
# Resolve old directory to absolute path
OLD_ABS=$(cd "$OLD_DIR" 2>/dev/null && pwd || echo "")
if [ -z "$OLD_ABS" ]; then
echo "Error: Old directory does not exist: $OLD_DIR"
exit 1
fi
# Check if new directory already exists
if [ -e "$NEW_DIR" ]; then
echo "Error: New directory already exists: $NEW_DIR"
exit 1
fi
# Convert paths to Claude's format (replace / and . with -)
# Need to do this BEFORE moving the directory to check for existing context
OLD_ENCODED=$(echo "$OLD_ABS" | sed 's/[\/.]/-/g')
# Figure out what the new absolute path will be
if [[ "$NEW_DIR" = /* ]]; then
# NEW_DIR is already absolute
NEW_ABS_FUTURE="$NEW_DIR"
else
# NEW_DIR is relative, resolve it
NEW_PARENT=$(dirname "$NEW_DIR")
NEW_BASENAME=$(basename "$NEW_DIR")
if [ "$NEW_PARENT" = "." ]; then
NEW_ABS_FUTURE="$(pwd)/$NEW_BASENAME"
else
NEW_ABS_FUTURE="$(cd "$NEW_PARENT" 2>/dev/null && pwd)/$NEW_BASENAME"
fi
fi
NEW_ENCODED=$(echo "$NEW_ABS_FUTURE" | sed 's/[\/.]/-/g')
CLAUDE_DIR="$HOME/.claude"
# Check if destination Claude context already exists BEFORE moving directory
EXISTING_CONTEXT=()
for subdir in projects file-history todos shell-snapshots debug; do
NEW_PATH="$CLAUDE_DIR/$subdir/$NEW_ENCODED"
if [ -d "$NEW_PATH" ] || [ -f "$NEW_PATH" ]; then
EXISTING_CONTEXT+=("$subdir")
fi
done
# If destination context exists, prompt user BEFORE moving anything
if [ ${#EXISTING_CONTEXT[@]} -gt 0 ]; then
echo "Warning: Claude context already exists for $NEW_ABS_FUTURE:"
for item in "${EXISTING_CONTEXT[@]}"; do
COUNT=""
if [ "$item" = "projects" ] && [ -d "$CLAUDE_DIR/$item/$NEW_ENCODED" ]; then
SESSION_COUNT=$(ls -1 "$CLAUDE_DIR/$item/$NEW_ENCODED"/*.jsonl 2>/dev/null | wc -l | tr -d ' ')
COUNT=" ($SESSION_COUNT sessions)"
fi
echo " - $item$COUNT"
done
echo ""
echo "Options:"
echo " [c] Clean out existing context and continue"
echo " [m] Merge old context into existing context"
echo " [n] Abort (default)"
echo ""
read -p "Choose [c/m/N]: " -n 1 -r
echo
if [[ $REPLY =~ ^[Cc]$ ]]; then
echo "Cleaning out existing context for $NEW_ABS_FUTURE..."
for item in "${EXISTING_CONTEXT[@]}"; do
NEW_PATH="$CLAUDE_DIR/$item/$NEW_ENCODED"
rm -rf "$NEW_PATH"
echo " ✓ Removed $item"
done
echo ""
elif [[ $REPLY =~ ^[Mm]$ ]]; then
echo "Will merge contexts..."
else
echo "Aborted."
exit 1
fi
fi
echo "Moving Claude context from:"
echo " $OLD_ABS"
echo " → $NEW_ABS_FUTURE"
echo ""
MOVED=0
# Rename/merge in each location if exists (BEFORE moving directory)
for subdir in projects file-history todos shell-snapshots debug; do
OLD_PATH="$CLAUDE_DIR/$subdir/$OLD_ENCODED"
NEW_PATH="$CLAUDE_DIR/$subdir/$NEW_ENCODED"
if [ -d "$OLD_PATH" ]; then
if [ -d "$NEW_PATH" ]; then
# Merge: move all files (including dotfiles) from old to new,
# skipping any name collisions and surfacing them to the user.
echo "Merging $subdir/$OLD_ENCODED → $subdir/$NEW_ENCODED"
(
shopt -s dotglob nullglob
for f in "$OLD_PATH"/*; do
base=$(basename "$f")
if [ -e "$NEW_PATH/$base" ]; then
echo " ⚠ collision, kept existing: $base" >&2
else
mv -- "$f" "$NEW_PATH/"
fi
done
)
rmdir "$OLD_PATH" 2>/dev/null || echo " ⚠ kept (collisions remain): $OLD_PATH" >&2
else
# Simple rename
echo "Moving $subdir/$OLD_ENCODED → $subdir/$NEW_ENCODED"
mv "$OLD_PATH" "$NEW_PATH"
fi
MOVED=$((MOVED + 1))
elif [ -f "$OLD_PATH" ]; then
echo "Moving $subdir/$OLD_ENCODED → $subdir/$NEW_ENCODED"
mv "$OLD_PATH" "$NEW_PATH"
MOVED=$((MOVED + 1))
fi
done
if [ $MOVED -eq 0 ]; then
echo "No Claude context found for $OLD_ABS"
else
echo "✓ Moved $MOVED context location(s)"
fi
echo ""
# Update session file contents to reference new path
if [ -d "$CLAUDE_DIR/projects/$NEW_ENCODED" ]; then
SESSION_DIR="$CLAUDE_DIR/projects/$NEW_ENCODED"
SESSION_FILES=$(find "$SESSION_DIR" -name "*.jsonl" 2>/dev/null)
if [ -n "$SESSION_FILES" ]; then
echo "Updating session file references..."
BACKUP_DIR="$CLAUDE_DIR/.claude-mv-backups"
mkdir -p "$BACKUP_DIR"
TS=$(date +%Y%m%d%H%M%S)
BACKUP_TAR="$BACKUP_DIR/projects-$NEW_ENCODED-$TS.tar.gz"
( cd "$CLAUDE_DIR/projects" && tar -czf "$BACKUP_TAR" -- "./$NEW_ENCODED" )
echo " ✓ backup: $BACKUP_TAR"
OLD_PAT=$(sed_escape_pattern "$OLD_ABS")
NEW_REP=$(sed_escape_replacement "$NEW_ABS_FUTURE")
# Only rewrite files that actually contain the old path, and preserve
# mtime so unrelated path-rewrites don't reshuffle session ordering.
UPDATED=0
find "$SESSION_DIR" -name "*.jsonl" -print0 | while IFS= read -r -d '' f; do
if grep -qF -- "$OLD_ABS" "$f"; then
ref=$(mktemp -t claude-mv-mtime)
touch -r "$f" "$ref"
sed -i '' "s|$OLD_PAT|$NEW_REP|g" "$f"
touch -r "$ref" "$f"
rm -f "$ref"
UPDATED=$((UPDATED + 1))
fi
done
echo "✓ Updated session files (mtime preserved)"
echo ""
fi
fi
# Move the actual directory AFTER Claude context is moved
echo "Moving directory:"
echo " $OLD_ABS"
echo " → $NEW_DIR"
mv "$OLD_ABS" "$NEW_DIR"
# Resolve new directory to absolute path
NEW_ABS=$(cd "$NEW_DIR" 2>/dev/null && pwd)
echo "✓ Directory moved"
echo ""
# Update history.jsonl to reference new path
HISTORY_FILE="$CLAUDE_DIR/history.jsonl"
if [ -f "$HISTORY_FILE" ]; then
echo ""
echo "Updating history.jsonl references..."
HIST_TS=$(date +%Y%m%d%H%M%S)
HISTORY_BACKUP="$HISTORY_FILE.backup.$HIST_TS"
cp -p "$HISTORY_FILE" "$HISTORY_BACKUP"
OLD_PAT=$(sed_escape_pattern "$OLD_ABS")
NEW_REP=$(sed_escape_replacement "$NEW_ABS")
if grep -qF -- "$OLD_ABS" "$HISTORY_FILE"; then
ref=$(mktemp -t claude-mv-mtime)
touch -r "$HISTORY_FILE" "$ref"
sed -i '' "s|$OLD_PAT|$NEW_REP|g" "$HISTORY_FILE"
touch -r "$ref" "$HISTORY_FILE"
rm -f "$ref"
echo "✓ Updated history.jsonl (backup: $HISTORY_BACKUP, mtime preserved)"
else
echo "✓ history.jsonl unchanged (no references found)"
rm -f "$HISTORY_BACKUP"
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment