You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SaveStateAsync resets a chmod 600 state file to 0644 and replaces a symlinked state file with a regular file (regression from the temp-file save, #105) #107
Since #105, JsonFilePersistenceProvider.SaveStateAsync writes to {path}.{guid}.tmp and then swaps it in (Navigation/Services/JsonFilePersistenceProvider.cs, lines ~60–83 at b56914b). The swap is File.Replace when the target exists and File.Move when it doesn't. The temp file is created with new FileStream(tempPath, FileMode.CreateNew, …), so on Unix it gets the umask default mode. Two things are lost on every save:
The file's permissions. A state file the user has restricted to 0600 becomes 0644 (group- and world-readable) after the next save. The state includes item metadata such as file paths.
A symlink. If the state path is a symlink, for example into a dotfiles repo, the rename replaces the link itself. The link is gone, and the real target never receives the new state.
The previous File.WriteAllTextAsync(_filePath, …) kept both.
Reproduction (Linux, .NET 10)
// SaveStateAsync, chmod 0600, navigate, SaveStateAsync again
before 2nd save: UserWrite, UserRead
after 2nd save: OtherRead, GroupRead, UserWrite, UserRead
// state.json -> symlink to real.json, then SaveStateAsync
state.json still a symlink: False; real.json content: {}
Suggested fix
On non-Windows, when the target exists, create the temp file with the target's mode. Use FileStreamOptions { Mode = FileMode.CreateNew, UnixCreateMode = File.GetUnixFileMode(_filePath), … }, or call File.SetUnixFileMode(tempPath, …) before the swap.
Resolve _filePath through new FileInfo(_filePath).ResolveLinkTarget(returnFinalTarget: true) and write the temp file next to the final target, so the link survives and the target gets the new content.
Acceptance criteria:
After saving over an existing 0600 state file, the file is still 0600.
After saving through a symlinked state path, the path is still a symlink and its target holds the new state.
Priority: High. Since Save navigation state through a temporary file [patch] #105, every SaveStateAsync resets a 0600 state file to group- and world-readable, and the state holds item metadata such as file paths. Saving through a symlinked state path destroys the link, and the real target never receives the state. Both happen on the ordinary save path.
Next step: Create the temp file with UnixCreateMode taken from the existing target. Resolve _filePath to its final link target and write the temp file next to that target. Add Unix-only tests for the three acceptance criteria.
Activity
matt-edmondson commentedon Nov 5, 2026
Triage
SaveStateAsyncresets a0600state file to group- and world-readable, and the state holds item metadata such as file paths. Saving through a symlinked state path destroys the link, and the real target never receives the state. Both happen on the ordinary save path.JsonFilePersistenceProvider.SaveStateAsync(Navigation/Services/JsonFilePersistenceProvider.cs), in the temp-file-and-swap logic from Save navigation state through a temporary file [patch] #105.File.Replace/File.Moveswap, so land it first and build this fix on top of it, or the two will conflict.UnixCreateModetaken from the existing target. Resolve_filePathto its final link target and write the temp file next to that target. Add Unix-only tests for the three acceptance criteria.Generated by Claude Code