Fix broken project paths in Claude Code and Codex
A macOS migration script discovers stale project paths in four Claude Code and Codex sources, then updates the related local state with explicit safety limits.
The problem
Claude Code and Codex can retain a project path in several local locations. Moving the directory does not rewrite those references.
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 locations the script updates:
| Store | Location | What breaks |
|---|---|---|
| Claude Code | ~/.claude/projects/<encoded-path>/ |
Session history, memory, project settings |
| Claude Code | ~/.claude/projects/*/.session-aliases |
Session aliases that point at the encoded directory |
| 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 |
The state spans encoded directory names, JSON, TOML, SQLite, and JSONL. A manual migration must preserve the same old-to-new mapping across every location that contains it.
What migrate-project does
migrate-project is a bash script with two different scopes. Four scanners discover stale roots in Claude project directories, .claude.json, Codex’s SQLite database, and config.toml. Once a mapping is selected, the migration also rewrites Codex global state and rollout files.
Install
curl -fsSL -o ~/.local/bin/migrate-project \
https://gist.githubusercontent.com/ravikanchikare/613ec7934b4c5736acea2914dfccd067/raw/migrate-project.sh
chmod +x ~/.local/bin/migrate-projectThe script targets macOS and requires bash, sqlite3, python3, awk, grep, and BSD sed. It also assumes ~/.local/bin is on $PATH.
Commands
See what is broken:
migrate-project listOutputs 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 discoverSame data, presented as a readable report with confidence levels for each suggestion.
Interactively fix each broken path:
migrate-project walkWalks 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. Flags must precede the command:
migrate-project --dry-run walkOne-off explicit migration:
migrate-project /old/project/path /new/project/pathMigrates a single path directly, without interactive prompting.
How it works
The script operates in three phases:
Discovery
Four scanners inspect Claude project directories, .claude.json, the Codex threads database, and config.toml. Each missing directory becomes a pipe-delimited record (path|source|thread_count), then duplicate paths are merged.
Suggestion
For each broken path, the script extracts the basename and checks five parent directories (~/Documents, ~/code, ~/workspace, ~/Desktop, ~). One match becomes an auto-suggestion; zero matches means only that no candidate exists in those locations.
Migration
When a mapping is confirmed, the script renames the encoded directory and updates session aliases, runs UPDATE on state_5.sqlite, uses sed for config.toml, global state, and rollout files, and uses python3 for .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.
The backup policy is partial. state_5.sqlite, config.toml, and .codex-global-state.json receive .bak copies before modification. The script does not back up the renamed Claude project directory, .claude.json, .session-aliases, or changed rollout JSONL files.
Design decisions
Keep the implementation inspectable. One shell file exposes every scanner and mutation path. That makes review easier, but it does not remove dependencies: python3 is not guaranteed on a stock macOS installation, and the sed -i '' calls are macOS-specific.
No stale-entry cleanup command. The walk command offers accept, manual, ignore, skip, and quit. It cannot delete an abandoned mapping. During a migration, however, migrate_claude removes the old encoded directory when the target encoded directory already exists.
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.
There is no complete automatic undo. The three Codex .bak files can restore those stores. A full rollback also requires the separate backup created before running the script.
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
Discovery and migration cover different surfaces
Four scanners find stale roots. A confirmed mapping also updates session aliases, Codex global state, and rollout files that discovery does not scan independently.
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.
Small scripts still have prerequisites
The implementation depends on bash, sqlite3, python3, awk, grep, and macOS-style sed. Check those commands before running it on a new machine.
Backup coverage is partial
The script backs up the Codex database, config, and global-state file. Claude project directories, .claude.json, and rollout JSONL files need a separate backup.