From 34e7c93f7894f5e331b40aed7834202bcbc91c1c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 8 Jun 2026 00:03:49 +0300 Subject: [PATCH 1/2] Fix: never persist runtime config overrides during project registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: `serena start-mcp-server --language-backend X --project ` silently rewrote the global serena_config.yml's language_backend to X. Cause: a not-yet-registered --project is auto-registered, and SerenaConfig.save() rewrote the WHOLE file from the in-memory config — which by then carried the CLI --language-backend override (and would equally carry --log-level / --tool-timeout / etc.). Fix: project (de)registration now persists ONLY the projects list. add_registered_project / remove_project go through a new _persist_projects(), which re-loads the on-disk config, copies in the current project list, and saves that — so a transient runtime override of any field can never be written back. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + src/serena/config/serena_config.py | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c89f63de..48c040e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Status of the `main` branch. Changes prior to the next official version change w or a `.git`, including worktree/submodule pointer files), instead of preferring an ancestor's `.serena/project.yml` over a closer `.git`. This previously bound CLI agents (Claude Code, Codex, Gemini) launched from inside a worktree to the parent repo, causing stale reads and misdirected edits. + - Fix: some CLI flags on start-mcp-server modified the global config (notably, language-backend). * Language Servers: - `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`. diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 03a18e72..92b2060d 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -1035,10 +1035,10 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): def add_registered_project(self, registered_project: RegisteredProject) -> None: """ - Adds a registered project, saving the configuration file. + Adds a registered project, persisting only the updated project list (see :meth:`_persist_projects`). """ self.projects.append(registered_project) - self.save() + self._persist_projects() def add_project_from_path(self, project_root: Path | str) -> "Project": """ @@ -1083,7 +1083,23 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): break else: raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}") - self.save() + self._persist_projects() + + def _persist_projects(self) -> None: + """ + Persist ONLY the registered-projects list, leaving every other setting at its on-disk value. + + Project (de)registration can happen while a session is running with transient runtime overrides applied + to this in-memory instance (e.g. ``start-mcp-server --language-backend`` / ``--log-level``). A full + :meth:`save` would write those overrides back to the global config, silently clobbering the user's + settings. Instead we re-load the persisted config, copy in the current project list, and save that — so + only ``projects`` is ever mutated on disk. + """ + if self.config_file_path is None: + return + persisted = SerenaConfig.from_config_file() + persisted.projects = list(self.projects) + persisted.save() def save(self) -> None: """ From a9cf93aaa3202bfa557aab6003f18dc2e63cbd9c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 8 Jun 2026 11:31:10 +0200 Subject: [PATCH 2/2] Improve handling of configuration file updates * Make SerenaConfig.save private, because it is not a method that should ever be called on a runtime instance; it is purely an internal operation * Refactor the CLI init command appropriately (which used the save method) * Improve comments to make usage limitations clear * Improve the changelog entry to make error conditions more concrete --- CHANGELOG.md | 3 ++- src/serena/cli.py | 4 +--- src/serena/config/serena_config.py | 32 +++++++++++++++++++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c040e0..3ba64b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ Status of the `main` branch. Changes prior to the next official version change w or a `.git`, including worktree/submodule pointer files), instead of preferring an ancestor's `.serena/project.yml` over a closer `.git`. This previously bound CLI agents (Claude Code, Codex, Gemini) launched from inside a worktree to the parent repo, causing stale reads and misdirected edits. - - Fix: some CLI flags on start-mcp-server modified the global config (notably, language-backend). + - Fix: CLI flags on `start-mcp-server` could incorrectly be saved to the global configuration file if the + list of projects was modified (triggering a save of the configuration with transient overrides applied) * Language Servers: - `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`. diff --git a/src/serena/cli.py b/src/serena/cli.py index 3b9ba581..98b8ed66 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -184,9 +184,7 @@ class TopLevelCommands(AutoRegisteringGroup): ) def init(language_backend: Literal["LSP", "JetBrains"] = "LSP") -> None: click.echo(f"\nSerena version: {serena_version()}\n") - serena_config = SerenaConfig.from_config_file() - serena_config.language_backend = LanguageBackend(language_backend) - serena_config.save() + serena_config = SerenaConfig.init(language_backend=LanguageBackend(language_backend)) click.echo(f"Configuration file: {serena_config.config_file_path}") click.echo(f"Language backend: {language_backend}") diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 92b2060d..15499fe3 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -957,7 +957,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): # re-save the configuration file if any migrations were performed if num_migrations > 0: log.info("Legacy configuration was migrated; re-saving configuration file") - instance.save() + instance._save() return instance @@ -985,6 +985,20 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): log.error(f"Error migrating configuration file: {e}") return None + @classmethod + def init(cls, language_backend: LanguageBackend) -> "SerenaConfig": + """ + Supports the config initialisation CLI command, allowing the user to configure fundamental settings before + the first launch. + + :param language_backend: the language backend to use + :return: the created SerenaConfig instance + """ + config = cls.from_config_file() + config.language_backend = language_backend + config._save() + return config + @cached_property def project_paths(self) -> list[str]: return sorted(str(project.project_root) for project in self.projects) @@ -1035,14 +1049,14 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): def add_registered_project(self, registered_project: RegisteredProject) -> None: """ - Adds a registered project, persisting only the updated project list (see :meth:`_persist_projects`). + Adds a registered project, persisting the updated project list """ self.projects.append(registered_project) self._persist_projects() def add_project_from_path(self, project_root: Path | str) -> "Project": """ - Add a new project to the Serena configuration from a given path, auto-generating the project + Adds a new project to the Serena configuration from a given path, auto-generating the project with defaults if it does not exist. Will raise a FileExistsError if a project already exists at the path. @@ -1087,7 +1101,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): def _persist_projects(self) -> None: """ - Persist ONLY the registered-projects list, leaving every other setting at its on-disk value. + Persists ONLY the registered-projects list, leaving every other setting at its on-disk value. Project (de)registration can happen while a session is running with transient runtime overrides applied to this in-memory instance (e.g. ``start-mcp-server --language-backend`` / ``--log-level``). A full @@ -1099,11 +1113,15 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): return persisted = SerenaConfig.from_config_file() persisted.projects = list(self.projects) - persisted.save() + persisted._save() - def save(self) -> None: + def _save(self) -> None: """ - Saves the configuration to the file from which it was loaded (if any) + Saves the full configuration to the file from which it was loaded (if any) + + NOTE: This method is private, because it is not usually safe to save a configuration instance used + at runtime, because it often contains transient overrides (e.g. specified through the CLI) + that should never be persisted back to the configuration file. """ if self.config_file_path is None: return