Fix broken project paths in Claude Code and Codex
When a project directory moves, Claude Code and Codex don't follow. A single script finds every stale reference across seven data stores and rewrites them to the new location.
| Gist | migrate-project.sh |
| Install | curl -fsSL -o ~/.local/bin/migrate-project <gist-url> && chmod +x ~/.local/bin/migrate-project |
The problem
Claude Code and Codex store project paths in seven different places. When a project directory is renamed or moved, none of them follow.
Session history vanishes. Thread-to-directory associations break. Project-level configuration points to directories that no longer exist. The tools silently lose track of projects, and the only visible symptom is that your history is gone.
The seven stores:
| Store | Location | What breaks |
|---|---|---|
| Claude Code | ~/.claude/projects/<encoded-path>/ | Session history, memory, project settings |
| Claude Code | ~/.claude.json (projects dict) | Project-level configuration |
| Claude Code | ~/.claude.json (githubRepoPaths) | GitHub-to-local-path mapping |
| Codex | ~/.codex/state_5.sqlite | Thread-to-CWD associations |
| Codex | ~/.codex/config.toml | [projects."path"] sections |
| Codex | ~/.codex/.codex-global-state.json | Workspace root references |
| Codex | ~/.codex/sessions/*/rollout-*.jsonl | Per-session cwd fields |
Two different tools, three different formats (JSON, TOML, SQLite), one encoding scheme that maps paths to directory names by replacing slashes with hyphens. Fixing a single moved project means touching all of them — and getting each one right.
Renaming ~/code/experiment to ~/Documents/experiment requires seven separate edits across four different file types. Doing it by hand is tedious. Getting it wrong means corrupted state.
What migrate-project does
migrate-project is a single bash script that finds every stale path across all seven stores and rewrites them to the new location. It runs discovery across Claude Code and Codex simultaneously, suggests where each project likely moved, and applies consistent updates to every store.
Install
curl -fsSL -o ~/.local/bin/migrate-project \
https://gist.githubusercontent.com/ravikanchikare/613ec7934b4c5736acea2914dfccd067/raw/migrate-project.sh
chmod +x ~/.local/bin/migrate-project Assumes ~/.local/bin is on $PATH. If not, add export PATH="$HOME/.local/bin:$PATH" to ~/.zshrc.
Commands
See what is broken:
migrate-project list Outputs a table of broken paths, their suggested new locations, which stores reference them, and how many threads are associated.
Detailed view with suggestions:
migrate-project discover Same data, presented as a readable report with confidence levels for each suggestion.
Interactively fix each broken path:
migrate-project walk Walks through each broken path one at a time. For each one, it shows the suggested new location and offers accept, manual entry, ignore, or skip. Add --dry-run to preview without writing.
One-off explicit migration:
migrate-project /old/project/path /new/project/path Migrates a single path directly, without interactive prompting.
How it works
The script operates in three phases:
Discovery
Four scanner functions crawl each data store, test whether every referenced path still exists on disk, and emit broken paths as a pipe-delimited stream (path|source|thread_count). A deduplication step merges paths that appear in multiple stores.
Suggestion
For each broken path, the script extracts the directory basename and searches common parent directories (~/Documents, ~/code, ~/workspace, ~/Desktop, ~). If exactly one matching directory exists, it is marked as an auto-suggestion. Zero matches means the directory was likely deleted.
Migration
When a mapping is confirmed, the script renames the encoded directory under ~/.claude/projects/, runs UPDATE on state_5.sqlite for all matching thread rows, uses sed to replace paths in config.toml, .codex-global-state.json, and session rollout files, and uses python3 to update the projects dict and githubRepoPaths in .claude.json.
Discovery in detail
The four scanners each target a different storage format:
scan_claude_projects reads ~/.claude/projects/ directory names. Since Claude Code encodes paths by replacing / with -, decoding is lossy — a directory named -Users-ravi-code-my-app could only be decoded by reversing the substitution, but hyphens in original path components create ambiguity. The scanner uses .claude.json as an authoritative lookup table (encoded name to real path), falling back to naive decode only when no mapping exists. Worktree directories are explicitly skipped.
scan_codex_sqlite runs a SQL query against state_5.sqlite to pull every distinct cwd value from the threads table, along with a count of how many threads reference each path. It checks each one against the filesystem.
scan_codex_config uses grep to extract paths from [projects."<path>"] section headers in config.toml.
scan_claude_json iterates over the projects dict keys in .claude.json, skipping worktree entries, and tests each key against the filesystem.
Results from all four scanners are merged through an awk-based deduplicator that combines source labels and sums thread counts for paths found in multiple stores.
Migration in detail
When a mapping is confirmed, run_migration calls three functions:
migrate_claude renames the encoded directory under ~/.claude/projects/ from the old encoded name to the new one. It also walks every .session-aliases file in other project directories and updates any alias pointing to the old directory.
migrate_codex handles four separate stores. SQLite gets an UPDATE threads SET cwd = 'new' WHERE cwd = 'old'. config.toml gets sed replacements for both the section header and any source = "old..." lines. .codex-global-state.json gets a global sed replacement. Session rollout files get matched via grep and patched individually.
migrate_claude_json uses python3 to read and rewrite .claude.json — moving the project key from old to new, and updating githubRepoPaths entries that reference the old path.
Every store gets a .bak backup before any write.
Design decisions
Zero dependencies beyond OS defaults. The script uses bash, sqlite3, and python3 — all three ship with macOS. No package manager, no runtime, no version lock-in. A script built on these primitives runs identically today and five years from now.
No delete command. The walk command offers accept, manual, ignore, skip, and quit — no delete. If a project is truly gone and stale references should be removed, that cleanup is manual. The tool only moves data, never removes it.
Lossy encoding requires an external source of truth. Claude Code encodes paths by replacing / with -. Since hyphens are legal in directory names, the encoding cannot be uniquely reversed. The script relies on .claude.json as the authoritative mapping. If .claude.json is missing or corrupted, the naive decode fallback may produce incorrect paths for directories with hyphens in their names.
Backup files are the undo mechanism. Rather than building a reverse-migration feature, the script creates .bak copies before every mutation. If a migration goes wrong, restoring the backups undoes it. After confirming everything works, clean up with rm -f ~/.codex/*.bak.
Worktrees are explicitly excluded. Worktree directories under ~/.claude/projects/ contain encoded paths with --claude-worktrees segments. These are temporary by nature and are skipped during discovery to avoid false positives.
Takeaways
Tools store paths in more places than you think
Claude Code and Codex together reference a project directory in seven distinct locations. Renaming a folder breaks all of them silently — session history disappears, thread associations vanish, configuration goes stale.
Discovery is the hard part
The migration logic is straightforward. Finding every broken reference across SQLite databases, JSON configs, TOML files, and encoded directory names — that is where the complexity lives.
Lossy encoding creates real constraints
Claude Code encodes paths by replacing slashes with hyphens, which means directories with hyphens in their names cannot be decoded unambiguously. An external source of truth (.claude.json) is necessary to resolve them.
Zero-dependency scripts age well
Bash, sqlite3, and python3 ship with macOS. No package manager, no runtime, no version compatibility issues. A script built on these primitives will still run unchanged years from now.
Backup before mutation is non-negotiable
Every store gets a .bak copy before any write. When a migration goes wrong — wrong target directory, typo in a path — the backup is the undo mechanism.