From 7ea9317a95831d76ee5d1a402fe06833f40d7d65 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 22 Apr 2026 12:57:37 +0200 Subject: [PATCH 01/12] Removed no longer used set_modes and tests for it --- src/serena/agent.py | 12 ------ test/serena/test_set_modes.py | 75 ----------------------------------- 2 files changed, 87 deletions(-) delete mode 100644 test/serena/test_set_modes.py diff --git a/src/serena/agent.py b/src/serena/agent.py index 295a4add..9331b26d 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -744,18 +744,6 @@ class SerenaAgent: raise ValueError("No active project. Please activate a project first.") return project - def set_modes(self, mode_names: list[str]) -> None: - """ - Set the current mode configurations. - - :param mode_names: List of mode names or paths to use - """ - self._mode_overrides = ModeSelectionDefinition(default_modes=mode_names) - self._update_active_modes() - self._update_active_tools() - - log.info(f"Set modes to {[mode.name for mode in self.get_active_modes()]}") - def get_active_modes(self) -> list[SerenaAgentMode]: """ :return: the list of active modes diff --git a/test/serena/test_set_modes.py b/test/serena/test_set_modes.py deleted file mode 100644 index dadae4ba..00000000 --- a/test/serena/test_set_modes.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Tests for SerenaAgent.set_modes() to verify that mode switching works correctly.""" - -import logging - -from serena.agent import SerenaAgent -from serena.config.serena_config import ModeSelectionDefinition, SerenaConfig - - -class TestSetModes: - """Test that set_modes correctly changes active modes.""" - - def _create_agent(self, modes: ModeSelectionDefinition | None = None) -> SerenaAgent: - config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) - return SerenaAgent(serena_config=config, modes=modes) - - def test_set_modes_changes_active_modes(self) -> None: - """Test that calling set_modes actually changes the active modes.""" - agent = self._create_agent(modes=ModeSelectionDefinition(default_modes=["editing", "interactive"])) - - initial_mode_names = sorted(m.name for m in agent.get_active_modes()) - assert "editing" in initial_mode_names - assert "interactive" in initial_mode_names - - # Switch to planning mode - agent.set_modes(["planning", "interactive"]) - - new_mode_names = sorted(m.name for m in agent.get_active_modes()) - assert "planning" in new_mode_names - assert "interactive" in new_mode_names - assert "editing" not in new_mode_names - - def test_set_modes_overrides_config_defaults(self) -> None: - """Test that set_modes takes precedence over config defaults.""" - config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) - config.default_modes = ["editing", "interactive"] - agent = SerenaAgent(serena_config=config) - - # Verify config defaults are active - initial_mode_names = [m.name for m in agent.get_active_modes()] - assert "editing" in initial_mode_names - - # Switch modes — should override config defaults - agent.set_modes(["planning", "one-shot"]) - - new_mode_names = [m.name for m in agent.get_active_modes()] - assert "planning" in new_mode_names - assert "one-shot" in new_mode_names - assert "editing" not in new_mode_names - - def test_set_modes_persists_after_repeated_calls(self) -> None: - """Test that set_modes result persists (modes don't revert).""" - agent = self._create_agent(modes=ModeSelectionDefinition(default_modes=["editing"])) - - agent.set_modes(["planning"]) - mode_names_1 = [m.name for m in agent.get_active_modes()] - assert "planning" in mode_names_1 - - # Call get_active_modes again — should still be planning - mode_names_2 = [m.name for m in agent.get_active_modes()] - assert mode_names_1 == mode_names_2 - - def test_set_modes_can_switch_back(self) -> None: - """Test that modes can be switched back to original after switching away.""" - agent = self._create_agent(modes=ModeSelectionDefinition(default_modes=["editing", "interactive"])) - - # Switch away - agent.set_modes(["planning", "one-shot"]) - assert "planning" in [m.name for m in agent.get_active_modes()] - - # Switch back - agent.set_modes(["editing", "interactive"]) - mode_names = [m.name for m in agent.get_active_modes()] - assert "editing" in mode_names - assert "interactive" in mode_names - assert "planning" not in mode_names From ee6a926eabf0998bc0ff717e44e5ec119761a350 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 24 Apr 2026 17:00:45 +0200 Subject: [PATCH 02/12] Add news item for v1.2.0 release --- news/20260424.html | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 news/20260424.html diff --git a/news/20260424.html b/news/20260424.html new file mode 100644 index 00000000..e60a1f27 --- /dev/null +++ b/news/20260424.html @@ -0,0 +1,18 @@ +
+

Serena v1.2.0

+

April 24, 2026

+

+ Interactive Debugging. + With the Serena JetBrains plugin, + your agent can set now set breakpoints, inspect variables, evaluate expressions and control execution flow + by directly interacting with the IDE's debugger. + Our REPL-based interface is highly flexible, allowing all of this to be provided via a single tool. +

+

+ Update Now. The update also adds JSON as a new supported "language" for the LSP backend, + improvements in token efficiency/prompt provision, as well as a multitude of general improvements and fixes. + See our change log for details.
+ If you are using the uv tool installation of Serena, upgrade as follows:
+ uv tool upgrade serena-agent --prerelease=allow +

+
\ No newline at end of file From 26f4fcdc9f7da55f16c77336299324cf3e52d8c6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 25 Apr 2026 19:17:02 +0200 Subject: [PATCH 03/12] CLI: make heavy imports local to accelerate light cli commands --- src/serena/cli.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 5e87fa2e..5001fb6d 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -18,7 +18,6 @@ from sensai.util.string import dict_string from tqdm import tqdm from serena import serena_version -from serena.agent import SerenaAgent from serena.config.client_setup import client_setup_handlers from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode from serena.config.serena_config import ( @@ -36,9 +35,6 @@ from serena.constants import ( SERENAS_OWN_CONTEXT_YAMLS_DIR, SERENAS_OWN_MODE_YAMLS_DIR, ) -from serena.mcp import SerenaMCPFactory -from serena.project import Project -from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool, ToolRegistry from serena.util.cli_util import AutoRegisteringGroup from serena.util.dataclass import get_dataclass_default from serena.util.logging import MemoryLogHandler @@ -315,6 +311,8 @@ class TopLevelCommands(AutoRegisteringGroup): trace_lsp_communication: bool | None, tool_timeout: float | None, ) -> None: + from serena.mcp import SerenaMCPFactory + # initialize logging, using INFO level initially (will later be adjusted by SerenaAgent according to the config) # * memory log handler (for use by GUI/Dashboard) # * stream handler for stderr (for direct console output, which will also be captured by clients like Claude Desktop) @@ -346,6 +344,7 @@ class TopLevelCommands(AutoRegisteringGroup): log.warning("No project root found from %s; not activating any project", os.getcwd()) project_file = project_file_arg or project + factory = SerenaMCPFactory(context=context, project=project_file, memory_log_handler=memory_log_handler) server = factory.create_mcp_server( host=host, @@ -394,6 +393,8 @@ class TopLevelCommands(AutoRegisteringGroup): def print_system_prompt( project: str, log_level: str, only_instructions: bool, context: str, modes: Sequence[str] | None = None ) -> None: + from serena.agent import SerenaAgent + prefix = "You will receive access to Serena's symbolic tools. Below are instructions for using them, take them into account." postfix = "You begin by acknowledging that you understood the above instructions and are ready to receive tasks." @@ -403,9 +404,14 @@ class TopLevelCommands(AutoRegisteringGroup): modes_selection_def: ModeSelectionDefinition | None = None if modes: modes_selection_def = ModeSelectionDefinition(default_modes=modes) + serena_config = SerenaConfig.from_config_file() + serena_config.web_dashboard = False + print(serena_config.default_modes) + print(serena_config.base_modes) + agent = SerenaAgent( project=os.path.abspath(project), - serena_config=SerenaConfig(web_dashboard=False, log_level=lvl), + serena_config=serena_config, context=context_instance, modes=modes_selection_def, ) @@ -828,6 +834,8 @@ class ProjectCommands(AutoRegisteringGroup): :param path: The path to check. :param project: The path to the project directory, defaults to the current working directory. """ + from serena.project import Project + serena_config = SerenaConfig.from_config_file() proj = Project.load(os.path.abspath(project), serena_config=serena_config) if os.path.isabs(path): @@ -851,6 +859,8 @@ class ProjectCommands(AutoRegisteringGroup): :param project: path to the project directory, defaults to the current working directory. :param verbose: if set, prints detailed information about the indexed symbols. """ + from serena.project import Project + serena_config = SerenaConfig.from_config_file() proj = Project.load(os.path.abspath(project), serena_config=serena_config) if os.path.isabs(file): @@ -887,6 +897,10 @@ class ProjectCommands(AutoRegisteringGroup): :param project: path to the project directory, defaults to the current working directory. """ # NOTE: completely written by Claude Code, only functionality was reviewed, not implementation + from serena.agent import SerenaAgent + from serena.project import Project + from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool + logging.configure(level=logging.INFO) project_path = os.path.abspath(project) serena_config = SerenaConfig.from_config_file() @@ -1040,6 +1054,8 @@ class ToolCommands(AutoRegisteringGroup): @click.option("--all", "-a", "include_optional", is_flag=True, help="List all tools, including those not enabled by default.") @click.option("--only-optional", is_flag=True, help="List only optional tools (those not enabled by default).") def list(quiet: bool = False, include_optional: bool = False, only_optional: bool = False) -> None: + from serena.tools import ToolRegistry + tool_registry = ToolRegistry() if quiet: if only_optional: @@ -1062,6 +1078,9 @@ class ToolCommands(AutoRegisteringGroup): @click.argument("tool_name", type=str) @click.option("--context", type=str, default=None, help="Context name or path to context file.") def description(tool_name: str, context: str | None = None) -> None: + from serena.agent import SerenaAgent + from serena.mcp import SerenaMCPFactory + # Load the context serena_context = None if context: From d3513077c8bf563ae9007325229b0c911a0a4c16 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 25 Apr 2026 19:18:08 +0200 Subject: [PATCH 04/12] CLI: added new command cc-system-prompt-override --- src/serena/cli.py | 10 ++ .../generated/generated_prompt_factory.py | 3 + .../config/prompt_templates/system_prompt.yml | 156 ++++++++++++++++++ 3 files changed, 169 insertions(+) diff --git a/src/serena/cli.py b/src/serena/cli.py index 5001fb6d..9872ccf0 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -35,6 +35,7 @@ from serena.constants import ( SERENAS_OWN_CONTEXT_YAMLS_DIR, SERENAS_OWN_MODE_YAMLS_DIR, ) +from serena.prompt_factory import SerenaPromptFactory from serena.util.cli_util import AutoRegisteringGroup from serena.util.dataclass import get_dataclass_default from serena.util.logging import MemoryLogHandler @@ -491,6 +492,15 @@ class TopLevelCommands(AutoRegisteringGroup): viewer = SerenaDashboardViewer(url, width=width, height=height) viewer.run() + @staticmethod + @click.command( + "cc-system-prompt-override", + help="To be used specifically in Claude Code as value for `--system-prompt`", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + def cc_system_prompt_override() -> None: + click.echo(SerenaPromptFactory().create_cc_system_prompt_override()) + class ModeCommands(AutoRegisteringGroup): """Group for 'mode' subcommands.""" diff --git a/src/serena/generated/generated_prompt_factory.py b/src/serena/generated/generated_prompt_factory.py index 43f866d7..bb947cea 100644 --- a/src/serena/generated/generated_prompt_factory.py +++ b/src/serena/generated/generated_prompt_factory.py @@ -33,3 +33,6 @@ class PromptFactory(PromptFactoryBase): mode_system_prompts: Any, ) -> str: return self._render_prompt("system_prompt", locals()) + + def create_cc_system_prompt_override(self) -> str: + return self._render_prompt("cc_system_prompt_override", locals()) diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index 426ba0be..722d3a9d 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -58,3 +58,159 @@ prompts: {% endfor %} You have hereby read the 'Serena Instructions Manual' and do not need to read it again. + + cc_system_prompt_override: | + You are Claude Code, Anthropic's official CLI for Claude. You are an interactive + software-engineering agent. The user works with you through a terminal; your text + output is what they see, and your tool calls are what change the world. + + # Tool selection (read this before every tool call on a code file) + + This project uses Serena, an MCP server that exposes semantic, symbol-aware tools + for reading and editing code. Serena's tools are the PRIMARY tools for code work + in this project. The built-in Read, Glob, Grep, and Edit tools are SECONDARY and + must not be used on code files when a Serena equivalent exists. + + The built-in tool descriptions in your context will tell you things like "use Read + for a known path" and "prefer dedicated tools (Read, Edit, Write, Glob, Grep)". + Those descriptions are written for projects without Serena and are SUPERSEDED here. + When they conflict with this section, this section wins. Do not rationalize the + built-in tools with "the file is small," "I already know what I need," "this is + one call versus three," or "the path is known" — those rationalizations have + produced incorrect behavior before and are explicitly disallowed. + + ## Mapping (use the right column, not the left) + + Task Tool to use + -------------------------------------- ---------------------------------------- + See a code file's structure get_symbols_overview + Read a specific symbol's body find_symbol (include_body=true) + Find a symbol by name across the repo find_symbol + Find references / callers find_referencing_symbols + Find declarations / implementations find_declaration / _find_implementations + Edit a symbol's body replace_symbol_body + Insert near a symbol insert_before_symbol / _insert_after_symbol + Pattern replace inside a file replace_content + Rename / move / delete a symbol rename / _move / _safe_delete + Inline a symbol inline_symbol + Type hierarchy type_hierarchy + + Built-in Read/Edit/Glob/Grep are permitted on code files ONLY when: + - Serena has been tried on the target and failed, OR + - The file is not parseable as code (e.g., generated, malformed), OR + - You need a regex search across many files that Serena's symbolic tools cannot + express — in which case Grep is acceptable as a discovery step, but follow-up + reads/edits on matched code files must still go through Serena. + - You need to read a few lines and symbolic reads would be an overkill. + - You absolutely have to read the full file for some reason. + + Read/Edit/Glob are fine for non-code files: markdown, JSON, YAML, TOML, .env, + config files, lockfiles, plain text, images. + + ## Required workflow before editing code + + 1. get_symbols_overview on the target file (skip if already done this session). + 2. find_symbol with include_body=true for the specific symbols you'll touch. + Read only the symbols you need — not the whole file. + 3. Edit with replace_symbol_body, insert_before_symbol, insert_after_symbol, or + replace_content. Never use the built-in Edit on a code file when one of these + fits. + + ## Self-check + + Before every Read, Glob, Grep, or Edit call: "Does this target a code file, and + does the mapping above name a Serena tool for this task?" If yes, switch. Do this + check every time — not just once per session. + + # Doing tasks + + The user will ask you to fix bugs, add features, refactor, explain code, and + similar. Approach each task with these defaults: + + - Understand before changing. Use the symbolic tools to build a precise picture of + what's there, then make the smallest change that satisfies the request. + - Don't add scope. No surrounding cleanup on a bug fix, no abstractions for + hypothetical future needs, no error handling for cases that can't happen, no + feature flags or backwards-compat shims unless asked. Three similar lines beats + a premature abstraction. + - Don't write comments unless the WHY is non-obvious — a hidden constraint, a + workaround, a subtle invariant. Don't narrate WHAT the code does; well-named + identifiers handle that. Don't reference the current task or PR in comments. + - Prefer editing existing files to creating new ones. Never create *.md or README + files unless the user explicitly asks. + - For exploratory questions ("what could we do about X?"), reply in 2–3 sentences + with a recommendation and the main tradeoff. Don't implement until the user + agrees. + - For UI/frontend changes you can't test in a browser, say so explicitly rather + than claiming success. + - Watch for security issues (injection, XSS, SQL injection, path traversal, secret + leaks). Fix them when you spot them. + + # Executing actions with care + + Local, reversible actions (editing files, running tests, reading state) are free + to take. Pause and confirm before: + + - Destructive ops: deleting files/branches, dropping tables, killing processes, + rm -rf, overwriting uncommitted changes, git reset --hard, force-push. + - Hard-to-reverse ops: amending published commits, removing dependencies, + modifying CI/CD. + - Externally visible actions: pushing, opening/closing/commenting on PRs or + issues, sending messages, posting to third-party services. + - Uploading content to third-party tools (renderers, pastebins) — assume it's + public and may be cached. + + If you hit an obstacle, find the root cause. Don't bypass it with --no-verify, + --force, or by deleting the thing in your way. If you find unfamiliar files, + branches, or config, investigate before deleting — it may be the user's + in-progress work. + + A user approving an action once does not approve it forever. Match the scope of + your action to what was actually requested. + + # Git and commits + + - Only commit when the user asks. Never proactively. + - Never update git config. Never skip hooks (--no-verify, --no-gpg-sign) unless + the user explicitly asks. + - Prefer new commits over --amend. If a pre-commit hook fails, the commit didn't + happen — fix the issue, re-stage, and create a NEW commit (not --amend, which + would modify the previous commit). + - Stage files by name, not `git add -A` or `git add .` — those can sweep in + secrets or large binaries. + - Don't commit files that look like secrets (.env, credentials.json, *.pem). If + the user explicitly asks, warn first. + - For commit messages, use a HEREDOC to preserve formatting. End the trailer with: + Co-Authored-By: Claude Opus 4.7 (1M context) + - Don't push unless asked. Never force-push to main/master; warn if asked. + - For PRs, use `gh` via Bash. Look at the full diff against the base branch (not + just the latest commit) before drafting title/body. + + # Tone and output + + - Your tool calls aren't visible to the user — only your text is. Before your + first tool call, say in one sentence what you're about to do. While working, + give short updates at key moments: a finding, a direction change, a blocker. + Brief is good; silent is not. + - Don't narrate internal deliberation. State results and decisions; skip the + thinking-aloud. + - End-of-turn summary: one or two sentences max. What changed, what's next. + Nothing else. + - Match response shape to the task: a simple question gets a direct answer, not + headers and sections. + - No emojis unless the user asks. + - Use Github-flavored markdown. Reference code locations as `path:line` so the + user can jump. + + # Parallel tool calls + + When tool calls don't depend on each other, issue them in a single response. + When they do depend on each other, issue them sequentially with the dependent + values resolved. Don't use placeholders or guess. + + # Asking for help vs. acting + + When a request is ambiguous in a way that materially changes the work, ask one + focused question. When it's only ambiguous in ways that don't change the work, + pick the reasonable interpretation and proceed — and say which interpretation + you picked. From d1e337f92410a4deee2307a6c544d63ec3c5bcb6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 25 Apr 2026 19:19:34 +0200 Subject: [PATCH 05/12] Extended claude-code context Adding more instructions and stressing them even more. The extension was suggested by claude code, then edited by me. This alone is not enough at the time of writing, the system prompt needs to be overridden for serena to be called consistently --- .../resources/config/contexts/claude-code.yml | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/serena/resources/config/contexts/claude-code.yml b/src/serena/resources/config/contexts/claude-code.yml index fe63878b..e6b27b7d 100644 --- a/src/serena/resources/config/contexts/claude-code.yml +++ b/src/serena/resources/config/contexts/claude-code.yml @@ -3,14 +3,35 @@ prompt: | You are running in a CLI coding agent context where file operations, basic (line-based) edits and reads as well as shell commands are handled by your own, internal tools. + You have access to Serena's code intelligence tools that exploit the symbolic + structure of the code and are much more efficient than your own tools for most coding scenarios. + If you are working on any coding task and if Serena's tools are deferred, you + should load them all immediately, before performing any read, grep or bash commands. If Serena's tools can be used to achieve your task, you should prioritize them. - **IMPORTANT**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them - right now. + **CRITICAL**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them + **right now**. For exploring and reading code in a token-efficient manner, use Serena's code-centric overview and symbolic search tools. For edits, prefer Serena's editing tools since they are more token efficient. - Especially when editing code you have read through Serena, ALWAYS use Serena's tools for editing, because your built-in tools will deny such edits (they will assume you haven't read the content). - **IMPORTANT**: Never use a tool before having read its schema via the tool-search tool! + Especially when editing code you have read through Serena, **ALWAYS** use Serena's tools for editing, because your built-in tools will deny such edits (they will assume you haven't read the content). + **CRITICAL**: Never use a tool before having read its schema via the tool-search tool! + **CRITICAL**: You have an preference for relying on your own tools like grep, read and edit. It is of paramount + importance that you overcome this preference and instead develop a preference for using Serena's tools since they + are superior for most coding tasks. Follow the following rules: + + For any code files: + - Read -> FORBIDDEN for discovery. Use get_symbols_overview, then find_symbol with include_body. + You may use read only if you already have an overview of the file and if reading whole symbols + is inappropriate (e.g. a few lines of code). + - Glob (by name) -> Allowed for discovery only. + - Grep (content) -> Allowed for discovery only; follow up reads or reference searches must be Serena. + - Edit -> FORBIDDEN. Use replace_symbol_body / insert_*_symbol / replace_content. + + Disallowed reasoning. Do NOT use any of the following to justify Read/Edit on a code file: + - "I already know the path" + - "one Read call is faster than three Serena calls" + - "the built-in tool description says to use Read for known paths" + If you catch yourself reaching for one of these, that is the signal to switch to Serena. excluded_tools: - create_text_file From f4b035698c228c271dd0f314ef214ef76d052e1a Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 25 Apr 2026 19:37:33 +0200 Subject: [PATCH 06/12] Docs: added explanation on system prompt override for CC --- docs/02-usage/030_clients.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index c65bfe0b..ba12e29e 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -101,9 +101,29 @@ You can do this in Tools / GitHub Copilot / Chat, where at the bottom you can cl ## Claude Code -Serena is a great way to make Claude Code both cheaper and more powerful! +Serena is a great way to make Claude Code both more efficient and more powerful! To add Serena to Claude Code, you can simply run `serena setup claude-code`. Alternatively, follow the instructions below. +:::{attention} +Recent updates to Claude Code (CC) and to the Opus line of models resulted in drastically poorer +adherence to instructions pertaining to Serena's tools. + +After extensive analysis, we identified part of the reason to be very long and detailed +tool descriptions for inbuilt tools and parts of the default system prompt. +The descriptions of CC's system tools take almost 16k tokens, cannot be adjusted by the user, +and introduce a very strong bias towards internal tools, making it near impossible to convince Opus 4.7 to use Serena. + +As a workaround, we crafted a system prompt that counteracts this bias. +When using Serena, we highly recommend that you start CC as + +```shell +claude --system-prompt=$(serena cc-system-prompt-override) +``` + +You can also consider adding the content of `serena cc-system-prompt-override` to your `CLAUDE.md` files, +but the effect be insufficient for counteracting Claude Code's bias towards internal tools. +::: + **Global Configuration**. To add the Serena MCP server for all your projects, use the user-level configuration of claude code and the `--project-from-cwd` flag: ```shell From 1a83060b05d31ca322b8f49e175f1bec6d5402e4 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 26 Apr 2026 16:25:58 +0200 Subject: [PATCH 07/12] Extended interprompt and moved cc-system-prompt-override command to prompts subcommand Also added print-prompt-template subcommand --- CHANGELOG.md | 8 ++++- docs/02-usage/030_clients.md | 2 +- src/interprompt/jinja_template.py | 3 ++ src/interprompt/multilang_prompt.py | 3 ++ src/interprompt/prompt_factory.py | 17 +++++++---- src/serena/cli.py | 29 +++++++++++++------ .../generated/generated_prompt_factory.py | 16 +++++++++- 7 files changed, 61 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea6203e..eb7bd2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. * General: - - Support `serena --version` CLI command for displaying the current version #1347 - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 - `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`) - Fix: Duplicate comments in re-saved YAML configuration files #1285 @@ -31,6 +30,13 @@ Status of the `main` branch. Changes prior to the next official version change w The `initial_instructions` tool provides the full prompt on demand, keeping the initial context lean. - Add `serena_info` tool for on-demand retrieval of usage information +* CLI: + - Support `serena --version` CLI command for displaying the current version #1347 + - Extend `prompts` subcommand with `print-prompt-template` and `print-cc-system-prompt-override`. + +* Clients: + - Document workaround to make Claude Code use Serena's tools after recent degradations caused by changes in CC harness and Opus 4.7 release. + * JetBrains: - Add `debug` tool: The agent can set breakpoints, inspect variables, evaluate expressions and control execution flow by directly interacting with the IDE's debugger, using a REPL-style interface for maximum flexibility. diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index ba12e29e..b1946bb7 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -117,7 +117,7 @@ As a workaround, we crafted a system prompt that counteracts this bias. When using Serena, we highly recommend that you start CC as ```shell -claude --system-prompt=$(serena cc-system-prompt-override) +claude --system-prompt="$(serena prompts print-cc-system-prompt-override)" ``` You can also consider adding the content of `serena cc-system-prompt-override` to your `CLAUDE.md` files, diff --git a/src/interprompt/jinja_template.py b/src/interprompt/jinja_template.py index 63f4fd87..32220155 100644 --- a/src/interprompt/jinja_template.py +++ b/src/interprompt/jinja_template.py @@ -30,6 +30,9 @@ class JinjaTemplate(ParameterizedTemplateInterface): parsed_content = self._template.environment.parse(self._template_string) self._parameters = sorted(jinja2.meta.find_undeclared_variables(parsed_content)) + def get_template_string(self) -> str: + return self._template_string + def render(self, **params: Any) -> str: """Renders the template with the given kwargs. You can find out which parameters are required by calling get_parameter_names().""" return self._template.render(**params) diff --git a/src/interprompt/multilang_prompt.py b/src/interprompt/multilang_prompt.py index e7322efb..3c84ced8 100644 --- a/src/interprompt/multilang_prompt.py +++ b/src/interprompt/multilang_prompt.py @@ -19,6 +19,9 @@ class PromptTemplate(ToStringMixin, ParameterizedTemplateInterface): def _tostring_exclude_private(self) -> bool: return True + def get_template_string(self) -> str: + return self._jinja_template.get_template_string() + def render(self, **params: Any) -> str: return self._jinja_template.render(**params) diff --git a/src/interprompt/prompt_factory.py b/src/interprompt/prompt_factory.py index 1769e7f2..9ad6ab7c 100644 --- a/src/interprompt/prompt_factory.py +++ b/src/interprompt/prompt_factory.py @@ -2,7 +2,7 @@ import logging import os from typing import Any -from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList +from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList, PromptTemplate log = logging.getLogger(__name__) @@ -23,9 +23,15 @@ class PromptFactoryBase: self.lang_code = lang_code self._prompt_collection = MultiLangPromptCollection(prompts_dir, fallback_mode=fallback_mode) + def _get_prompt_template(self, prompt_name: str) -> PromptTemplate: + return self._prompt_collection.get_prompt_template(prompt_name, lang_code=self.lang_code) + + def get_prompt_template_string(self, prompt_name: str) -> str: + return self._get_prompt_template(prompt_name).get_template_string() + def _render_prompt(self, prompt_name: str, params: dict[str, Any]) -> str: del params["self"] - return self._prompt_collection.render_prompt_template(prompt_name, params, lang_code=self.lang_code) + return self._get_prompt_template(prompt_name).render(**params) def _get_prompt_list(self, prompt_name: str) -> PromptList: return self._prompt_collection.get_prompt_list(prompt_name, self.lang_code) @@ -41,14 +47,12 @@ def autogenerate_prompt_factory_module(prompts_dir: str, target_module_path: str :param prompts_dir: the directory containing the prompt templates and prompt lists :param target_module_path: the path to the target module file (.py). Important: The module will be overwritten! """ - generated_code = """ -# ruff: noqa + generated_code = """# ruff: noqa # black: skip # mypy: ignore-errors # NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually! -from interprompt.multilang_prompt import PromptList from interprompt.prompt_factory import PromptFactoryBase from typing import Any @@ -68,6 +72,9 @@ class PromptFactory(PromptFactoryBase): else: method_params_str = ", *, " + ", ".join([f"{param}: Any" for param in template_parameters]) generated_code += f""" + def get_{template_name}_template_string(self) -> str: + return self.get_prompt_template_string('{template_name}') + def create_{template_name}(self{method_params_str}) -> str: return self._render_prompt('{template_name}', locals()) """ diff --git a/src/serena/cli.py b/src/serena/cli.py index 9872ccf0..6d25a35b 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -492,15 +492,6 @@ class TopLevelCommands(AutoRegisteringGroup): viewer = SerenaDashboardViewer(url, width=width, height=height) viewer.run() - @staticmethod - @click.command( - "cc-system-prompt-override", - help="To be used specifically in Claude Code as value for `--system-prompt`", - context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, - ) - def cc_system_prompt_override() -> None: - click.echo(SerenaPromptFactory().create_cc_system_prompt_override()) - class ModeCommands(AutoRegisteringGroup): """Group for 'mode' subcommands.""" @@ -1200,6 +1191,26 @@ class PromptCommands(AutoRegisteringGroup): os.remove(user_prompt_yaml_path) click.echo(f"Deleted override file '{prompt_yaml_name}'.") + @staticmethod + @click.command( + "print-prompt-template", + help="prints the (unrendered) template for the corresponding prompt name. " + "This respects custom prompt yaml overrides and thus will print the value that will be used in Serena", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + @click.argument("prompt_name", type=str) + def print_prompt_template(prompt_name: str) -> None: + click.echo(SerenaPromptFactory().get_prompt_template_string(prompt_name)) + + @staticmethod + @click.command( + "print-cc-system-prompt-override", + help="To be used specifically in Claude Code as value for `--system-prompt`", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + def print_cc_system_prompt_override() -> None: + click.echo(SerenaPromptFactory().get_cc_system_prompt_override_template_string()) + _mode = ModeCommands() _context = ContextCommands() diff --git a/src/serena/generated/generated_prompt_factory.py b/src/serena/generated/generated_prompt_factory.py index bb947cea..a1ee2920 100644 --- a/src/serena/generated/generated_prompt_factory.py +++ b/src/serena/generated/generated_prompt_factory.py @@ -4,7 +4,6 @@ # NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually! -from interprompt.multilang_prompt import PromptList from interprompt.prompt_factory import PromptFactoryBase from typing import Any @@ -14,15 +13,27 @@ class PromptFactory(PromptFactoryBase): A class for retrieving and rendering prompt templates and prompt lists. """ + def get_info_jet_brains_debug_repl_template_string(self) -> str: + return self.get_prompt_template_string("info_jet_brains_debug_repl") + def create_info_jet_brains_debug_repl(self) -> str: return self._render_prompt("info_jet_brains_debug_repl", locals()) + def get_onboarding_prompt_template_string(self) -> str: + return self.get_prompt_template_string("onboarding_prompt") + def create_onboarding_prompt(self, *, system: Any) -> str: return self._render_prompt("onboarding_prompt", locals()) + def get_connection_prompt_template_string(self) -> str: + return self.get_prompt_template_string("connection_prompt") + def create_connection_prompt(self) -> str: return self._render_prompt("connection_prompt", locals()) + def get_system_prompt_template_string(self) -> str: + return self.get_prompt_template_string("system_prompt") + def create_system_prompt( self, *, @@ -34,5 +45,8 @@ class PromptFactory(PromptFactoryBase): ) -> str: return self._render_prompt("system_prompt", locals()) + def get_cc_system_prompt_override_template_string(self) -> str: + return self.get_prompt_template_string("cc_system_prompt_override") + def create_cc_system_prompt_override(self) -> str: return self._render_prompt("cc_system_prompt_override", locals()) From d4c75224d349aa44e5eef90f5f7bad62abb2ebc2 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 26 Apr 2026 16:26:30 +0200 Subject: [PATCH 08/12] Added demo script for profiling/debugging the CLI --- scripts/demo_cli_call.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 scripts/demo_cli_call.py diff --git a/scripts/demo_cli_call.py b/scripts/demo_cli_call.py new file mode 100644 index 00000000..9639a9a8 --- /dev/null +++ b/scripts/demo_cli_call.py @@ -0,0 +1,4 @@ +from serena.cli import top_level + +if __name__ == "__main__": + top_level(["--help"]) From 621ca5cf44c246c10298937af135ab27fc788891 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 27 Apr 2026 12:19:36 +0200 Subject: [PATCH 09/12] Interprompt: Add location to prompt templates, allow to retrieve template --- src/interprompt/multilang_prompt.py | 8 +++++--- src/interprompt/prompt_factory.py | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/interprompt/multilang_prompt.py b/src/interprompt/multilang_prompt.py index 3c84ced8..ba953b4c 100644 --- a/src/interprompt/multilang_prompt.py +++ b/src/interprompt/multilang_prompt.py @@ -12,8 +12,9 @@ log = logging.getLogger(__name__) class PromptTemplate(ToStringMixin, ParameterizedTemplateInterface): - def __init__(self, name: str, jinja_template_string: str) -> None: + def __init__(self, name: str, jinja_template_string: str, path: str) -> None: self.name = name + self.path = path self._jinja_template = JinjaTemplate(jinja_template_string.strip()) def _tostring_exclude_private(self) -> bool: @@ -252,6 +253,7 @@ class MultiLangPromptCollection: self, name: str, template_str: str, + path: str, lang_code: str = DEFAULT_LANG_CODE, on_name_collision: Literal["skip", "overwrite", "raise"] = "raise", ) -> None: @@ -262,7 +264,7 @@ class MultiLangPromptCollection: :param on_name_collision: how to deal with name/lang_code collisions """ allow_overwrite = False - prompt_template = PromptTemplate(name, template_str) + prompt_template = PromptTemplate(name, template_str, path=path) mlpt = self._multi_lang_prompt_templates.get(name) if mlpt is None: mlpt = MultiLangPromptTemplate(name) @@ -330,7 +332,7 @@ class MultiLangPromptCollection: self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision) elif isinstance(prompt_template_or_list, str): self._add_prompt_template( - prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision + prompt_name, prompt_template_or_list, lang_code=lang_code, path=path, on_name_collision=on_name_collision ) else: raise ValueError( diff --git a/src/interprompt/prompt_factory.py b/src/interprompt/prompt_factory.py index 9ad6ab7c..647b0ed2 100644 --- a/src/interprompt/prompt_factory.py +++ b/src/interprompt/prompt_factory.py @@ -23,15 +23,18 @@ class PromptFactoryBase: self.lang_code = lang_code self._prompt_collection = MultiLangPromptCollection(prompts_dir, fallback_mode=fallback_mode) - def _get_prompt_template(self, prompt_name: str) -> PromptTemplate: + def get_prompt_names(self) -> list[str]: + return self._prompt_collection.get_prompt_template_names() + + def get_prompt_template(self, prompt_name: str) -> PromptTemplate: return self._prompt_collection.get_prompt_template(prompt_name, lang_code=self.lang_code) def get_prompt_template_string(self, prompt_name: str) -> str: - return self._get_prompt_template(prompt_name).get_template_string() + return self.get_prompt_template(prompt_name).get_template_string() def _render_prompt(self, prompt_name: str, params: dict[str, Any]) -> str: del params["self"] - return self._get_prompt_template(prompt_name).render(**params) + return self.get_prompt_template(prompt_name).render(**params) def _get_prompt_list(self, prompt_name: str) -> PromptList: return self._prompt_collection.get_prompt_list(prompt_name, self.lang_code) From 2382399b9237bb885f099192b0a48c4a28be45fb Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 27 Apr 2026 12:19:49 +0200 Subject: [PATCH 10/12] Improve prompt list CLI function --- CHANGELOG.md | 2 +- src/serena/cli.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7bd2bc..8e85a398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ Status of the `main` branch. Changes prior to the next official version change w * CLI: - Support `serena --version` CLI command for displaying the current version #1347 - - Extend `prompts` subcommand with `print-prompt-template` and `print-cc-system-prompt-override`. + - Extend `prompts` subcommand with `print-prompt-template` and `print-cc-system-prompt-override`, improve `list` subcommand * Clients: - Document workaround to make Claude Code use Serena's tools after recent degradations caused by changes in CC harness and Opus 4.7 release. diff --git a/src/serena/cli.py b/src/serena/cli.py index 6d25a35b..e7407545 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -1109,16 +1109,26 @@ class PromptCommands(AutoRegisteringGroup): @staticmethod @click.command( - "list", help="Lists yamls that are used for defining prompts.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH} + "list", help="Lists prompt names and YAML files that can be overridden.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH} ) def list() -> None: + # list prompt names + click.echo("Prompts:") + factory = SerenaPromptFactory() + for key in factory.get_prompt_names(): + template = factory.get_prompt_template(key) + is_overridden = not template.path.startswith(PROMPT_TEMPLATES_DIR_INTERNAL) + click.echo(f" * '{key}' ({template.path if is_overridden else 'default'})") + + # list prompts files + click.echo("\nPrompt files (which you can override with the create-override command):") serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")] for prompt_yaml_name in serena_prompt_yaml_names: user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name) if os.path.exists(user_prompt_yaml_path): - click.echo(f"{user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}") + click.echo(f" * {user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}") else: - click.echo(prompt_yaml_name) + click.echo(f" * {prompt_yaml_name}") @staticmethod @click.command( @@ -1209,7 +1219,7 @@ class PromptCommands(AutoRegisteringGroup): context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, ) def print_cc_system_prompt_override() -> None: - click.echo(SerenaPromptFactory().get_cc_system_prompt_override_template_string()) + click.echo(SerenaPromptFactory().create_cc_system_prompt_override()) _mode = ModeCommands() From d9864bcdc0f0e8053fd6647f8d4fe4b8992df5ad Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 27 Apr 2026 13:16:20 +0200 Subject: [PATCH 11/12] Update news for 1.2.0 --- news/{20260424.html => 20260427.html} | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) rename news/{20260424.html => 20260427.html} (60%) diff --git a/news/20260424.html b/news/20260427.html similarity index 60% rename from news/20260424.html rename to news/20260427.html index e60a1f27..0cdba681 100644 --- a/news/20260424.html +++ b/news/20260427.html @@ -1,6 +1,6 @@

Serena v1.2.0

-

April 24, 2026

+

April 27, 2026

Interactive Debugging. With the Serena JetBrains plugin, @@ -9,9 +9,18 @@ Our REPL-based interface is highly flexible, allowing all of this to be provided via a single tool.

- Update Now. The update also adds JSON as a new supported "language" for the LSP backend, + Counteracting Claude Code Regressions. + We analysed the issues causing Claude Code to use external tools increasingly suboptimally and implemented + another workaround (in addition to the existing hooks): a system prompt override. + Read our updated Claude code usage instructions for more information. +

+

+ The update also adds JSON as a new supported "language" for the LSP backend, improvements in token efficiency/prompt provision, as well as a multitude of general improvements and fixes. See our change log for details.
+

+

+ Update Now. If you are using the uv tool installation of Serena, upgrade as follows:
uv tool upgrade serena-agent --prerelease=allow

From 7c50041d149fe0f6980ebe6add0b34a27547907b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 27 Apr 2026 13:32:23 +0200 Subject: [PATCH 12/12] Improve Claude Code client setup documentation --- docs/02-usage/030_clients.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index b1946bb7..646873b1 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -102,16 +102,20 @@ You can do this in Tools / GitHub Copilot / Chat, where at the bottom you can cl ## Claude Code Serena is a great way to make Claude Code both more efficient and more powerful! -To add Serena to Claude Code, you can simply run `serena setup claude-code`. Alternatively, follow the instructions below. +To set up the Serena MCP server for Claude Code, you can simply run this command: + + serena setup claude-code + +Find manual setup instructions as well as workarounds for Claude Code's recent regressions pertaining to (external) tool use below. :::{attention} -Recent updates to Claude Code (CC) and to the Opus line of models resulted in drastically poorer +Recent updates to Claude Code (CC) and to the Opus line of models resulted in drastically reduced adherence to instructions pertaining to Serena's tools. After extensive analysis, we identified part of the reason to be very long and detailed -tool descriptions for inbuilt tools and parts of the default system prompt. +tool descriptions for built-in tools and parts of the default system prompt. The descriptions of CC's system tools take almost 16k tokens, cannot be adjusted by the user, -and introduce a very strong bias towards internal tools, making it near impossible to convince Opus 4.7 to use Serena. +and introduce a very strong bias towards internal tools, making it almost impossible to convince Opus 4.7 to use Serena. As a workaround, we crafted a system prompt that counteracts this bias. When using Serena, we highly recommend that you start CC as