Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions nerve/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from nerve import paths
from nerve.config import _expand_path, _interpolate_str, workspace_settings_file
from nerve.utils.fs import atomic_write_text
from nerve.workspace import (
initialize_workspace,
install_bundled_skills,
Expand Down Expand Up @@ -452,9 +453,7 @@ def _save_init_state(choices: SetupChoices, completed: set[str]) -> None:
"saved_at": datetime.now().isoformat(timespec="seconds"),
}
path = _init_state_file()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(state), encoding="utf-8")
os.chmod(path, 0o600) # contains API keys
atomic_write_text(path, json.dumps(state), mode=0o600)
except OSError:
pass

Expand Down
36 changes: 36 additions & 0 deletions tests/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,42 @@ def test_state_file_permissions(self) -> None:
mode = stat.S_IMODE(os.stat(path).st_mode)
assert mode == 0o600

@pytest.mark.skipif(
not hasattr(os, "fchmod"),
reason="requires fchmod to isolate the post-write chmod failure",
)
def test_state_file_stays_private_if_chmod_fails(self, monkeypatch) -> None:
from nerve.bootstrap import _init_state_file, _save_init_state

def fail_chmod(*args, **kwargs):
raise OSError("chmod failed")

monkeypatch.setattr(os, "chmod", fail_chmod)
previous_umask = os.umask(0)
try:
_save_init_state(SetupChoices(), {"mode"})
finally:
os.umask(previous_umask)

path = _init_state_file()
assert stat.S_IMODE(os.stat(path).st_mode) == 0o600

def test_failed_replace_preserves_existing_checkpoint(self, monkeypatch) -> None:
from nerve.bootstrap import _init_state_file, _save_init_state

_save_init_state(SetupChoices(), {"old"})
path = _init_state_file()
previous = path.read_text(encoding="utf-8")

def fail_replace(*args, **kwargs):
raise OSError("replace failed")

monkeypatch.setattr(os, "replace", fail_replace)
_save_init_state(SetupChoices(), {"new"})

assert path.read_text(encoding="utf-8") == previous
assert list(path.parent.glob(f".{path.name}.*.tmp")) == []

def test_choices_from_dict_ignores_unknown_keys(self) -> None:
from nerve.bootstrap import _choices_from_dict

Expand Down
Loading