Skip to content

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

Open

Description

@matt-edmondson

What's wrong

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:

  1. 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.
  2. 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.
  • The atomicity of Save navigation state through a temporary file [patch] #105 is unchanged: the target is never truncated in place.

Keybinding's atomic save has the same pattern and is filed separately.

Activity

  1. matt-edmondson commented on Nov 5, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions