<# rename_project_folder.ps1 (written 2026-10-13, waiting mode added 2026-10-14) Renames the project's root folder and repairs what names its path. The user asked for the name "HADAL:The Descent". Windows does not allow ":" in a file or folder name, so the default new name is "HADAL - The Descent". Pass -NewName to use another spelling. Windows refuses to rename a folder while any process has its working directory inside it (checked: "Access to the path is denied"). Every Claude session in the folder, its helper processes and the app's terminal tabs do, so no session can rename its own folder. Two ways to run it: by hand, with the Claude app closed (Win+R): powershell -NoExit -NoProfile -ExecutionPolicy Bypass -File "D:\SubnauticaCLONE\.tools\rename_project_folder.ps1" waiting in the background (started by a session, outside the app's process tree): -RetrySeconds 1800 -Popup tries every 3 seconds until the folder is free (the user quits the Claude app), then tells the result in a box. What it does, in this order: 1. checks the new name and shows what still holds the folder 2. renames the folder 3. leaves a hidden link (junction) under the old name, so that sessions, open Blender files, .blend files and logs that name the old path keep working (-NoLink leaves no link) 4. user environment variables that name the folder (UE_SDKS_ROOT) 5. paths written into the project's own scripts, manifests and docs. Not touched: logs, backups, versions, the frozen browser versions, Assets, Unreal's generated folders, GameLore.txt, Game_decisions.txt 6. Claude's project folder (memory, session transcripts): the new path's key becomes a link to the existing one, so sessions in the old and the new path share one memory. Paths in memory files are rewritten 7. Unreal's per-user project lists (the Epic launcher reads them), with a backup 8. the Obsidian vault's live notes (Reference, Workflows, Rules, hub notes; chat notes stay as written) 9. a section in CLAUDE.md, a log in .tools, and the run appended to the vault chat note -DryRun changes nothing and lists what would be changed. To undo: -OldPath "" -NewName "" (remove the hidden link first: cmd /c rmdir "") #> param( [string]$OldPath = 'D:\SubnauticaCLONE', [string]$NewName = 'HADAL - The Descent', [string]$VaultRoot = 'D:\AI\Vault\EagerEagle\The Descent', [string]$VaultNote = 'D:\AI\Vault\EagerEagle\The Descent\Chats\2026-10-13 Project folder rename to HADAL - The Descent.md', [string]$ClaudeProjects = (Join-Path $env:USERPROFILE '.claude\projects'), [string]$ClaudeSessions = (Join-Path $env:USERPROFILE '.claude\sessions'), [string]$UeUserRoot = (Join-Path $env:LOCALAPPDATA 'UnrealEngine'), [int]$RetrySeconds = 0, # >0: keep trying this long without asking (background mode) [string]$StatusFile = '', # every line is also appended here (to watch a background run) [switch]$Popup, # tell the result in a message box [switch]$AlreadyRenamed, # the folder was renamed by hand: skip step 2, do the repairs (3 to 9) [switch]$NoLink, [switch]$DryRun, [switch]$NoWait ) $ErrorActionPreference = 'Stop' # This process must not sit inside the folder either. Set-Location -LiteralPath $env:SystemRoot [Environment]::CurrentDirectory = $env:SystemRoot $Log = New-Object System.Collections.Generic.List[string] function Say([string]$m) { Write-Host $m; $Log.Add($m) if ($StatusFile) { try { [IO.File]::AppendAllText($StatusFile, $m + "`r`n") } catch { } } } function Warn([string]$m) { Say ('WARN ' + $m) } function Show-Popup([string]$text) { if (-not $Popup) { return } try { Add-Type -AssemblyName System.Windows.Forms [void][System.Windows.Forms.MessageBox]::Show($text, 'Project folder rename', 'OK', 'Information', 'Button1', 'DefaultDesktopOnly') } catch { } } $ReparsePoint = [IO.FileAttributes]::ReparsePoint function Test-Link([string]$p) { if (-not (Test-Path -LiteralPath $p)) { return $false } return [bool]((Get-Item -LiteralPath $p -Force).Attributes -band $ReparsePoint) } # Claude Code names a project's folder after its path with every character that is not a letter or digit turned # into "-" (checked against the existing keys: D:\Games II\STALKER -> D--Games-II-STALKER, E:\Tools\UE_5.8 -> E--Tools-UE-5-8). function Get-ClaudeKey([string]$p) { return ($p -replace '[^a-zA-Z0-9]', '-') } $OldPath = [IO.Path]::GetFullPath($OldPath).TrimEnd('\') $Parent = Split-Path -Parent $OldPath $OldName = Split-Path -Leaf $OldPath $NewPath = Join-Path $Parent $NewName $OldKey = Get-ClaudeKey $OldPath $NewKey = Get-ClaudeKey $NewPath $Stamp = Get-Date -Format 'yyyy-MM-dd' $Mode = '' if ($DryRun) { $Mode = ' (dry run: nothing is changed)' } Say ("rename_project_folder {0}{1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm'), $Mode) Say (" from $OldPath") Say (" to $NewPath") # ---------------------------------------------------------------- 1. checks function Stop-Here([string]$m) { Say ('STOP ' + $m); Show-Popup ("Not renamed.`n`n" + $m); exit 2 } if ($NewName.IndexOfAny([IO.Path]::GetInvalidFileNameChars()) -ge 0 -or $NewName -ne $NewName.Trim() -or $NewName.EndsWith('.')) { Stop-Here 'the new name is not a valid Windows folder name (none of \ / : * ? " < > | and no dot or space at the end).' } if ($AlreadyRenamed) { if (-not (Test-Path -LiteralPath $NewPath -PathType Container)) { Stop-Here "$NewPath does not exist." } if ((Test-Path -LiteralPath $OldPath) -and -not (Test-Link $OldPath)) { Stop-Here "$OldPath still exists as a real folder." } } else { if (-not (Test-Path -LiteralPath $OldPath -PathType Container)) { Stop-Here "$OldPath does not exist." } if (Test-Link $OldPath) { Stop-Here "$OldPath is already a link, so the rename seems to be done." } if (Test-Path -LiteralPath $NewPath) { Stop-Here "$NewPath already exists." } } function Test-Under([string]$p) { if (-not $p) { return $false } $p = $p.TrimEnd('\') return ($p -ieq $OldPath) -or $p.StartsWith($OldPath + '\', [StringComparison]::OrdinalIgnoreCase) } # The working directory of another process, read from its process parameters (64-bit processes of this user). $CanReadCwd = $true try { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; using System.Text; public static class ProcCwd { [StructLayout(LayoutKind.Sequential)] struct PBI { public IntPtr R1; public IntPtr Peb; public IntPtr R2a; public IntPtr R2b; public IntPtr Pid; public IntPtr R3; } [DllImport("ntdll.dll")] static extern int NtQueryInformationProcess(IntPtr h, int cls, ref PBI pbi, int len, out int retLen); [DllImport("kernel32.dll")] static extern IntPtr OpenProcess(int access, bool inherit, int pid); [DllImport("kernel32.dll")] static extern bool ReadProcessMemory(IntPtr h, IntPtr addr, byte[] buf, IntPtr size, out IntPtr read); [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h); public static string Get(int pid) { IntPtr h = OpenProcess(0x0410, false, pid); if (h == IntPtr.Zero) return null; try { PBI pbi = new PBI(); int rl; IntPtr rd; if (NtQueryInformationProcess(h, 0, ref pbi, Marshal.SizeOf(pbi), out rl) != 0 || pbi.Peb == IntPtr.Zero) return null; byte[] b = new byte[8]; if (!ReadProcessMemory(h, (IntPtr)(pbi.Peb.ToInt64() + 0x20), b, (IntPtr)8, out rd)) return null; long pp = BitConverter.ToInt64(b, 0); byte[] us = new byte[16]; if (!ReadProcessMemory(h, (IntPtr)(pp + 0x38), us, (IntPtr)16, out rd)) return null; int len = BitConverter.ToUInt16(us, 0); long buf = BitConverter.ToInt64(us, 8); if (len <= 0 || len > 65534) return null; byte[] s = new byte[len]; if (!ReadProcessMemory(h, (IntPtr)buf, s, (IntPtr)len, out rd)) return null; return Encoding.Unicode.GetString(s); } catch { return null; } finally { CloseHandle(h); } } } '@ } catch { $CanReadCwd = $false } # Processes whose working directory is inside the folder: these are what makes Windows refuse the rename. function Get-Holders { $found = New-Object System.Collections.Generic.List[string] $sessions = @{} if (Test-Path -LiteralPath $ClaudeSessions) { foreach ($f in Get-ChildItem -LiteralPath $ClaudeSessions -Filter *.json) { $sp = 0 if ([int]::TryParse($f.BaseName, [ref]$sp)) { $sessions[$sp] = $true } } } foreach ($p in Get-CimInstance Win32_Process) { $id = [int]$p.ProcessId if ($id -eq $PID) { continue } $cwd = $null if ($CanReadCwd) { $cwd = [ProcCwd]::Get($id) } if (Test-Under $cwd) { $what = $p.Name if ($sessions.ContainsKey($id)) { $what = $p.Name + ' (a Claude session)' } $found.Add(("{0}, process {1}, working in {2}" -f $what, $id, $cwd.TrimEnd('\'))) } } return ,$found } # Programs showing a file of the folder in their window title (Blender). They do not stop the rename and keep # working through the link; without a link their file would be gone. function Get-OpenWindows { $found = New-Object System.Collections.Generic.List[string] foreach ($p in Get-Process) { if ($p.Id -ne $PID -and $p.MainWindowTitle -and $p.MainWindowTitle.IndexOf($OldPath, [StringComparison]::OrdinalIgnoreCase) -ge 0) { $found.Add(("{0}, process {1} ""{2}""" -f $p.Name, $p.Id, $p.MainWindowTitle)) } } return ,$found } function Show-Holders { $h = Get-Holders if ($h.Count -gt 0) { Say 'Working inside the folder (Windows refuses the rename while they run):'; foreach ($x in $h) { Say " - $x" } } $w = Get-OpenWindows if ($w.Count -gt 0) { Say 'Showing a file of the folder (not in the way; they keep working through the link):'; foreach ($x in $w) { Say " - $x" } } return ($h.Count + $w.Count) } # ---------------------------------------------------------------- text rules # Files are patched as bytes (Latin-1 round trip), so their encoding, BOM and line ends stay as they are. # UTF-16 files (some Unreal .ini) are decoded properly. $Latin = [Text.Encoding]::GetEncoding(28591) function As-ByteText([string]$s) { return $Latin.GetString([Text.Encoding]::UTF8.GetBytes($s)) } function New-Rules([bool]$byteText) { # A path is only rewritten where the folder name ends: not D:\SubnauticaCLONE2, not D:\SubnauticaCLONE.zip. $tail = '(?![\w-]|\.\w)' $rules = New-Object System.Collections.ArrayList $pairs = @( @($OldPath.Replace('\', '\\'), $NewPath.Replace('\', '\\')), # "D:\\name" in JSON and Python strings @($OldPath, $NewPath), @($OldPath.Replace('\', '/'), $NewPath.Replace('\', '/')), @($OldKey, $NewKey) # Claude's project key ) foreach ($p in $pairs) { $o = $p[0]; $n = $p[1] if ($byteText) { $o = As-ByteText $o; $n = As-ByteText $n } [void]$rules.Add(@((New-Object regex ('(?i)' + [regex]::Escape($o) + $tail)), $n.Replace('$', '$$'))) } return ,$rules } $RulesText = New-Rules $false $RulesBytes = New-Rules $true $OldNameBytes = As-ByteText $OldName $OldKeyBytes = As-ByteText $OldKey function Update-TextFile([string]$file, [string]$backupTo) { $res = New-Object psobject -Property @{ N = 0; Left = $false } $bytes = [IO.File]::ReadAllBytes($file) if ($bytes.Length -lt 2) { return $res } $utf16 = ($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) if ($utf16) { $text = [Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2); $rules = $RulesText; $bare = $OldName; $key = $OldKey } else { $text = $Latin.GetString($bytes); $rules = $RulesBytes; $bare = $OldNameBytes; $key = $OldKeyBytes } $ic = [StringComparison]::OrdinalIgnoreCase if ($text.IndexOf($bare, $ic) -lt 0 -and $text.IndexOf($key, $ic) -lt 0) { return $res } $n = 0 foreach ($r in $rules) { $c = $r[0].Matches($text).Count if ($c -gt 0) { $text = $r[0].Replace($text, $r[1]); $n += $c } } $res.N = $n $res.Left = ($text.IndexOf($bare, $ic) -ge 0) if ($n -gt 0 -and -not $DryRun) { if ($backupTo) { Copy-Item -LiteralPath $file -Destination $backupTo -Force } if ($utf16) { $body = [Text.Encoding]::Unicode.GetBytes($text) $ms = New-Object IO.MemoryStream $ms.Write([byte[]](0xFF, 0xFE), 0, 2); $ms.Write($body, 0, $body.Length) [IO.File]::WriteAllBytes($file, $ms.ToArray()) } else { [IO.File]::WriteAllBytes($file, $Latin.GetBytes($text)) } } return $res } $TextExt = '.ps1', '.psm1', '.bat', '.cmd', '.py', '.json', '.md', '.txt', '.ini', '.toml', '.yaml', '.yml', '.cfg', '.cs', '.sh', '.uproject', '.uplugin' $SkipDirs = 'Intermediate', 'Saved', 'DerivedDataCache', 'Binaries', 'Content', '_backups', 'backups', 'versions', 'the-descent', 'descent_v2', 'sub-v0', 'Assets', 'source_textures', '.git', 'node_modules', '__pycache__' $SkipFiles = 'GameLore.txt', 'Game_decisions.txt' # the user's own files function Get-TextFiles([string]$root, [bool]$recurse) { $stack = New-Object System.Collections.Stack $stack.Push($root) while ($stack.Count -gt 0) { $d = $stack.Pop() try { if ($recurse) { foreach ($sub in [IO.Directory]::GetDirectories($d)) { $di = New-Object IO.DirectoryInfo $sub if ($di.Attributes -band $ReparsePoint) { continue } # never walk through a link if ($SkipDirs -contains $di.Name) { continue } $stack.Push($sub) } } foreach ($f in [IO.Directory]::GetFiles($d)) { $fi = New-Object IO.FileInfo $f if (($TextExt -contains $fi.Extension.ToLower()) -and $fi.Length -lt 20MB -and ($SkipFiles -notcontains $fi.Name)) { $f } } } catch { } } } function Update-Set([string]$label, $files, [string]$relativeTo) { $changed = 0; $total = 0 $left = New-Object System.Collections.Generic.List[string] foreach ($f in $files) { if ($Self -contains $f) { continue } try { $r = Update-TextFile $f '' } catch { Warn "could not patch $f : $($_.Exception.Message)"; continue } $shown = $f if ($relativeTo -and $f.StartsWith($relativeTo + '\', [StringComparison]::OrdinalIgnoreCase)) { $shown = $f.Substring($relativeTo.Length + 1) } if ($r.N -gt 0) { $changed++; $total += $r.N; Say (" {0,4} {1}" -f $r.N, $shown) } if ($r.Left) { $left.Add($shown) } } Say (" {0}: {1} place(s) in {2} file(s)" -f $label, $total, $changed) return ,$left } # ---------------------------------------------------------------- 2. rename $Root = $OldPath if ($AlreadyRenamed) { $Root = $NewPath Say 'the folder was renamed by hand already; doing the repairs.' if (Test-Link $OldPath) { Say "link under the old name is there: $OldPath" } elseif ($NoLink) { Say 'no link under the old name (-NoLink).' } elseif ($DryRun) { Say "would leave a hidden link $OldPath -> $NewPath" } else { try { New-Item -ItemType Junction -Path $OldPath -Target $NewPath | Out-Null $li = Get-Item -LiteralPath $OldPath -Force $li.Attributes = $li.Attributes -bor [IO.FileAttributes]::Hidden Say "hidden link left: $OldPath -> $NewPath" } catch { Warn "the link under the old name could not be made: $($_.Exception.Message)" } } } elseif ($DryRun) { [void](Show-Holders) Say "would rename the folder" if (-not $NoLink) { Say "would leave a hidden link $OldPath -> $NewPath" } } else { if ($RetrySeconds -gt 0) { Say "waiting up to $RetrySeconds s for the folder to be free (quit the Claude app) ..." } $deadline = (Get-Date).AddSeconds($RetrySeconds) $first = $true while ($true) { $err = '' if ($NoLink -and (Get-OpenWindows).Count -gt 0) { $err = 'with -NoLink no program may show a file of the folder' } else { try { [IO.Directory]::Move($OldPath, $NewPath) } catch { $err = $_.Exception.Message } } if (-not $err) { break } if ($RetrySeconds -gt 0) { if ((Get-Date) -lt $deadline) { Start-Sleep -Seconds 3; continue } Say "NOT renamed, time is up: $err" [void](Show-Holders) Show-Popup ("The project folder was NOT renamed: something still worked inside it when the waiting time ran out.`n`nNothing was changed. Ask Claude to start it again.") exit 3 } Say "NOT renamed: $err" [void](Show-Holders) Say 'Quit the Claude app (every session and terminal tab of this folder holds it) and close what is listed,' Say 'then press Enter to try again. Ctrl+C gives up; nothing was changed.' if ($NoWait) { exit 3 } [void](Read-Host) } $Root = $NewPath Say ("renamed at {0}." -f (Get-Date -Format 'HH:mm:ss')) # ------------------------------------------------------------ 3. link under the old name if (-not $NoLink) { try { New-Item -ItemType Junction -Path $OldPath -Target $NewPath | Out-Null $li = Get-Item -LiteralPath $OldPath -Force $li.Attributes = $li.Attributes -bor [IO.FileAttributes]::Hidden Say "hidden link left: $OldPath -> $NewPath" } catch { Warn "the link under the old name could not be made: $($_.Exception.Message)" } } } # The script itself keeps its text (its defaults and its notes name the old path on purpose). $Self = @() if ($PSCommandPath) { $Self += $PSCommandPath if (Test-Under $PSCommandPath) { $Self += ($NewPath + $PSCommandPath.Substring($OldPath.Length)) } } # ---------------------------------------------------------------- 4. environment variables try { $vars = [Environment]::GetEnvironmentVariables('User') foreach ($k in @($vars.Keys)) { $v = [string]$vars[$k] if ($v.IndexOf($OldPath, [StringComparison]::OrdinalIgnoreCase) -ge 0) { $nv = $RulesText[1][0].Replace($v, $RulesText[1][1]) if ($nv -ne $v) { if (-not $DryRun) { [Environment]::SetEnvironmentVariable($k, $nv, 'User'); Set-Item -Path "Env:$k" -Value $nv } Say "user variable $k = $nv" } } } } catch { Warn "environment variables: $($_.Exception.Message)" } # ---------------------------------------------------------------- 5. the project's own files Say 'project files:' $left = Update-Set 'project' (Get-TextFiles $Root $true) $Root foreach ($name in 'CLAUDE.md', 'AGENTS.md') { # The first line of these two carries the folder's name as a title. $f = Join-Path $Root $name if (-not (Test-Path -LiteralPath $f)) { continue } $t = $Latin.GetString([IO.File]::ReadAllBytes($f)) $cut = $t.IndexOf("`n"); if ($cut -lt 0) { $cut = $t.Length } $first = $t.Substring(0, $cut) if ($first.Contains($OldNameBytes)) { if (-not $DryRun) { [IO.File]::WriteAllBytes($f, $Latin.GetBytes($first.Replace($OldNameBytes, (As-ByteText $NewName)) + $t.Substring($cut))) } Say " title line of $name" } } $stillNamed = @($left | Where-Object { $_ -notmatch '^(CLAUDE|AGENTS)\.md$' }) if ($stillNamed.Count -gt 0) { Say ' the old name is still written, outside a path, in:' foreach ($s in $stillNamed) { Say " $s" } } # ---------------------------------------------------------------- 6. Claude's project folder (memory, transcripts) # Nothing is moved. The key of the new path becomes a link to the folder of the old key, so a session finds the # same memory and the same transcripts whether it was started in the old path (through the link) or the new one. $memNote = 'Claude''s project folder (memory, session transcripts) was not found under `{OLDPROJ}`.' try { $oldProj = Join-Path $ClaudeProjects $OldKey $newProj = Join-Path $ClaudeProjects $NewKey if (-not (Test-Path -LiteralPath $oldProj)) { Say "Claude project folder: none under $OldKey" } elseif (Test-Link $oldProj) { Say "Claude project folder: $OldKey is itself a link, left alone" $memNote = 'Claude''s project folder `{OLDPROJ}` is a link; nothing was changed there.' } elseif (Test-Link $newProj) { Say "Claude project folder: $NewKey is a link already" $memNote = 'Claude''s project folder `{NEWPROJ}` is a link to `{OLDPROJ}` (memory and transcripts shared).' } elseif (Test-Path -LiteralPath $newProj) { # A session was already started in the new path, so its key is a real folder. Share the memory folder only: # it moves to the new key and the old key links to it. $oldMem = Join-Path $oldProj 'memory' $newMem = Join-Path $newProj 'memory' $memNote = 'Claude''s memory is `{NEWPROJ}\memory`; `{OLDPROJ}\memory` is a link to it. Transcripts of sessions started before the rename are still in `{OLDPROJ}`.' if ((-not (Test-Path -LiteralPath $oldMem)) -or (Test-Link $oldMem)) { Say "Claude memory: nothing to move from $OldKey" } elseif ((Test-Path -LiteralPath $newMem) -and -not (Test-Link $newMem) -and @(Get-ChildItem -LiteralPath $newMem -Force).Count -gt 0) { Warn "Claude memory: both $OldKey\memory and $NewKey\memory hold files; nothing moved, merge them by hand" $memNote = 'Claude has TWO memory folders, `{OLDPROJ}\memory` and `{NEWPROJ}\memory`; they have to be merged by hand.' } elseif ($DryRun) { Say "would move Claude's memory: $OldKey\memory -> $NewKey\memory, and link the old one to it" } else { if (Test-Path -LiteralPath $newMem) { [IO.Directory]::Delete($newMem) } # an empty folder or an old link [IO.Directory]::Move($oldMem, $newMem) New-Item -ItemType Junction -Path $oldMem -Target $newMem | Out-Null Say "Claude memory moved to $newMem ; $OldKey\memory links to it" } } elseif ($DryRun) { Say "would link Claude's project folder: $NewKey -> $OldKey (memory and transcripts shared)" } else { New-Item -ItemType Junction -Path $newProj -Target $oldProj | Out-Null Say "Claude project folder: $NewKey is a link to $OldKey (memory and transcripts shared)" $memNote = 'Claude''s project folder (memory, session transcripts) is still `{OLDPROJ}`; `{NEWPROJ}` is a link to it, so sessions started in the old and in the new path share one memory. Both spellings of the memory path are valid.' } # Memory files of every project, and the global CLAUDE.md, may name the path. $memFiles = @() foreach ($proj in Get-ChildItem -LiteralPath $ClaudeProjects -Directory -ErrorAction SilentlyContinue) { if (Test-Link $proj.FullName) { continue } $m = Join-Path $proj.FullName 'memory' if ((Test-Path -LiteralPath $m) -and -not (Test-Link $m)) { $memFiles += @(Get-TextFiles $m $false) } } $global = Join-Path (Split-Path -Parent $ClaudeProjects) 'CLAUDE.md' if (Test-Path -LiteralPath $global) { $memFiles += $global } Say 'Claude memory files:' [void](Update-Set 'memory' $memFiles $ClaudeProjects) } catch { Warn "Claude project folder: $($_.Exception.Message)" } # ---------------------------------------------------------------- 7. Unreal's per-user project lists try { $ueFiles = @() if (Test-Path -LiteralPath $UeUserRoot) { $ueFiles += @(Get-ChildItem -Path (Join-Path $UeUserRoot '*\Saved\Config\WindowsEditor\EditorSettings.ini') -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) $rec = Join-Path $UeUserRoot 'Editor\ProjectEditorRecords.json' if (Test-Path -LiteralPath $rec) { $ueFiles += $rec } } $bakDir = Join-Path $Root '.tools\backups' foreach ($f in $ueFiles) { $bak = Join-Path $bakDir ("{0}_{1}_before_folder_rename{2}" -f $Stamp, [IO.Path]::GetFileNameWithoutExtension($f), [IO.Path]::GetExtension($f)) if (-not $DryRun) { New-Item -ItemType Directory -Force -Path $bakDir | Out-Null } $r = Update-TextFile $f $bak if ($r.N -gt 0) { Say ("Unreal: {0} place(s) in {1}" -f $r.N, $f) } } if (Get-Process -Name UnrealEditor -ErrorAction SilentlyContinue) { Warn 'an Unreal editor is open. It may write its old project list back when it closes.' } } catch { Warn "Unreal settings: $($_.Exception.Message)" } # ---------------------------------------------------------------- 8. the vault's live notes try { if (Test-Path -LiteralPath $VaultRoot) { $vf = @(Get-TextFiles $VaultRoot $false) foreach ($sub in 'Reference', 'Workflows', 'Rules') { $d = Join-Path $VaultRoot $sub if (Test-Path -LiteralPath $d) { $vf += @(Get-TextFiles $d $true) } } Say 'vault notes (chat notes stay as written):' [void](Update-Set 'vault' $vf $VaultRoot) } } catch { Warn "vault: $($_.Exception.Message)" } # ---------------------------------------------------------------- 9. records if ($DryRun) { Say 'dry run finished. Nothing was changed.'; exit 0 } $section = @' ## Folder rename ({DATE}) The project folder was renamed from `{OLD}` to `{NEW}` at the user's request. The user wrote "HADAL:The Descent"; Windows does not allow ":" in a folder name. {HOW} - {LINK} - Use the new path in all new work. A build started through the old path makes UnrealBuildTool recompile the module, because the project then seems to be somewhere else. - {MEM} - `UE_SDKS_ROOT` is `{NEW}\.tools\AutoSDK` (the user variable and the tool scripts). - Paths in logs, backups, `versions\` folders and the vault's chat notes still say `{OLD}`: they are records. '@ $linkLine = "``{OLD}`` still exists as a **hidden link (junction)** to the new folder, so that sessions, .blend files and logs that name the old path keep working. Never delete it with a recursive delete. When the user says the old sessions are finished, remove only the link: ``cmd /c rmdir ""{OLD}""``." if ($NoLink) { $linkLine = 'No link was left under the old name: anything that still names `{OLD}` is broken and has to be pointed at the new path.' } $how = 'Done by `.tools\rename_project_folder.ps1`, log beside it.' if ($AlreadyRenamed) { $how = 'The user renamed the folder by hand; `.tools\rename_project_folder.ps1 -AlreadyRenamed` did the repairs, log beside it.' } $section = $section.Replace('{HOW}', $how).Replace('{LINK}', $linkLine).Replace('{MEM}', $memNote).Replace('{DATE}', $Stamp).Replace('{OLD}', $OldPath).Replace('{NEW}', $NewPath) $section = $section.Replace('{OLDPROJ}', (Join-Path $ClaudeProjects $OldKey)).Replace('{NEWPROJ}', (Join-Path $ClaudeProjects $NewKey)) $utf8 = New-Object Text.UTF8Encoding $false try { $claudeMd = Join-Path $Root 'CLAUDE.md' if (Test-Path -LiteralPath $claudeMd) { [IO.File]::AppendAllText($claudeMd, $section.Replace("`r`n", "`n"), $utf8); Say 'CLAUDE.md: section "Folder rename" added' } } catch { Warn "CLAUDE.md: $($_.Exception.Message)" } Say 'done.' Say "Open new Claude sessions in $NewPath . The first Unreal build there recompiles the game module." try { $toolsDir = Join-Path $Root '.tools' New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null [IO.File]::AppendAllText((Join-Path $toolsDir 'rename_project_folder.log'), (($Log -join "`r`n") + "`r`n`r`n"), $utf8) if (Test-Path -LiteralPath $VaultNote) { $block = "`n## Script run " + (Get-Date -Format 'yyyy-MM-dd HH:mm') + "`n" + '```text' + "`n" + ($Log -join "`n") + "`n" + '```' + "`n" [IO.File]::AppendAllText($VaultNote, $block, $utf8) } } catch { Write-Host "WARN log: $($_.Exception.Message)" -ForegroundColor Yellow } $warns = @($Log | Where-Object { $_ -like 'WARN*' }).Count $tail = '' if ($warns -gt 0) { $tail = "`n`n$warns warning(s): see .tools\rename_project_folder.log" } Show-Popup ("The project folder is now`n$NewPath`n`nYou can open Claude again. Your sessions keep working." + $tail) exit 0