Skip to content

Instantly share code, notes, and snippets.

@maleta
Last active August 1, 2026 00:12
Show Gist options
  • Select an option

  • Save maleta/a71b54c121c262221f7e32ff45db32c1 to your computer and use it in GitHub Desktop.

Select an option

Save maleta/a71b54c121c262221f7e32ff45db32c1 to your computer and use it in GitHub Desktop.
Rename a project folder and update Claude Code sessions to follow (rewrites ~/.claude/projects and history.jsonl)

claude-rename

Rename a project folder without losing your Claude Code session history.

What it does

When you rename a project folder, Claude Code loses track of it sessions, history, and project settings all reference the old path. This script:

  1. Renames the actual folder
  2. Renames the corresponding ~/.claude/projects/ directory
  3. Updates path references in ~/.claude/history.jsonl

Install

curl -fsSL https://gist.githubusercontent.com/maleta/a71b54c121c262221f7e32ff45db32c1/raw/claude-rename -o
/usr/local/bin/claude-rename
chmod +x /usr/local/bin/claude-rename

Or just download and put it anywhere on your $PATH.

Usage

claude-rename ~/projects/old-name ~/projects/new-name

Supports relative paths, ~, and .. - all resolved automatically.

Requirements

  • Bash 4+
  • macOS or Linux
#!/usr/bin/env bash
set -euo pipefail
CLAUDE_DIR="${HOME}/.claude"
usage() {
echo "Usage: claude-rename <source> <dest>"
echo ""
echo "Rename a project folder and update Claude Code sessions to follow."
echo ""
echo "Arguments:"
echo " source Current project folder path"
echo " dest New project folder path"
echo ""
echo "Examples:"
echo " claude-rename ~/projects/old-name ~/projects/new-name"
echo " claude-rename /abs/path/old /abs/path/new"
exit 1
}
# --- helpers ---
# Convert absolute path to Claude's encoded folder name (slashes → dashes)
encode_path() {
echo "$1" | sed 's|/|-|g'
}
log() {
echo "→ $*"
}
warn() {
echo "⚠ $*" >&2
}
die() {
echo "✗ $*" >&2
exit 1
}
# Resolve to absolute path (works on both macOS and Linux, no python)
resolve_path() {
local target="$1"
# Expand ~ manually
if [[ "$target" == "~"* ]]; then
target="${HOME}${target#"~"}"
fi
# If relative, prepend pwd
if [[ "$target" != /* ]]; then
target="$(pwd)/$target"
fi
# Normalize . and .. using pure bash
# Split on /, rebuild skipping . and resolving ..
local IFS='/' parts=() normalized=()
read -ra parts <<< "$target"
for part in "${parts[@]}"; do
case "$part" in
''|'.') ;;
'..') [[ ${#normalized[@]} -gt 0 ]] && unset 'normalized[${#normalized[@]}-1]' ;;
*) normalized+=("$part") ;;
esac
done
local result=""
for part in "${normalized[@]}"; do
result="${result}/${part}"
done
echo "${result:-/}"
}
# --- main ---
[[ $# -ne 2 ]] && usage
src_raw="$1"
dst_raw="$2"
src="$(resolve_path "$src_raw")"
dst="$(resolve_path "$dst_raw")"
log "Source: $src"
log "Dest: $dst"
# Validate source exists
[[ -d "$src" ]] || die "Source directory does not exist: $src"
# Validate dest does not exist
[[ -e "$dst" ]] && die "Destination already exists: $dst"
# Validate dest parent exists
dst_parent="$(dirname "$dst")"
[[ -d "$dst_parent" ]] || die "Destination parent directory does not exist: $dst_parent"
# Encode paths for Claude's project directory names
src_encoded="$(encode_path "$src")"
dst_encoded="$(encode_path "$dst")"
log "Encoded source: $src_encoded"
log "Encoded dest: $dst_encoded"
# 1. Rename the actual folder
log "Renaming folder: $src → $dst"
mv "$src" "$dst"
log "Folder renamed."
# 2. Rename Claude's project directory if it exists
projects_dir="${CLAUDE_DIR}/projects"
if [[ -d "${projects_dir}/${src_encoded}" ]]; then
log "Renaming Claude project dir: ${src_encoded} → ${dst_encoded}"
mv "${projects_dir}/${src_encoded}" "${projects_dir}/${dst_encoded}"
log "Claude project dir renamed."
else
warn "No Claude project directory found for: ${src_encoded} (skipping)"
fi
# 3. Update history.jsonl - replace project path references
history_file="${CLAUDE_DIR}/history.jsonl"
if [[ -f "$history_file" ]]; then
count=$(grep -c "\"$src\"" "$history_file" 2>/dev/null || true)
if [[ "$count" -gt 0 ]]; then
log "Updating history.jsonl ($count entries)..."
# Use a temp file for atomic replacement
tmp_file="$(mktemp)"
# Escape paths for sed (handle slashes)
src_escaped="${src//\//\\/}"
dst_escaped="${dst//\//\\/}"
sed "s|\"project\":\"${src}\"|\"project\":\"${dst}\"|g" "$history_file" > "$tmp_file"
mv "$tmp_file" "$history_file"
log "history.jsonl updated."
else
log "No matching entries in history.jsonl (skipping)."
fi
else
warn "history.jsonl not found (skipping)."
fi
echo ""
echo "Done. Claude sessions for '$src' now point to '$dst'."
@x241c297f

Copy link
Copy Markdown

This script fails if the source directory contains dots.
Claude encodes these with dashes as well.

Replace encode_path with

encode_path() {
  echo "$1" | sed 's|/|-|g; s|\.|-|g'
}

Not sure, whether there are more special characters that require such treatment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment