All articles
tips and-tricks · intermediate ·

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.

automationclaude-codeclicodexdeveloper-experiencetooling
Resources
Gistmigrate-project.sh
Installcurl -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:

Where project paths are stored
StoreLocationWhat 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.sqliteThread-to-CWD associations
Codex~/.codex/config.toml[projects."path"] sections
Codex~/.codex/.codex-global-state.jsonWorkspace root references
Codex~/.codex/sessions/*/rollout-*.jsonlPer-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

terminal bash
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:

terminal bash
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:

terminal bash
migrate-project discover

Same data, presented as a readable report with confidence levels for each suggestion.

Interactively fix each broken path:

terminal bash
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:

terminal bash
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.