From ee12b7265c4aa03982767cb3100cd095570e1ba9 Mon Sep 17 00:00:00 2001 From: shanevcantwell <153727980+shanevcantwell@users.noreply.github.com> Date: Wed, 24 Dec 2025 21:21:03 -0700 Subject: [PATCH 1/3] feat: add tool prompt override support in .continuerc.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `tools` array to config that allows users to override tool descriptions at the repo level, following the existing pattern for models, contextProviders, and slashCommands. This enables teams to customize tool prompts (e.g., standardizing path format instructions) without forking the codebase. Example .continuerc.json: ```json { "tools": [ { "name": "read_file", "description": "Custom description here" } ] } ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- core/config/load.ts | 2 + core/config/profile/doLoadConfig.ts | 13 ++ core/config/types.ts | 38 +++++- core/index.d.ts | 30 +++++ core/tools/applyToolOverrides.test.ts | 184 ++++++++++++++++++++++++++ core/tools/applyToolOverrides.ts | 69 ++++++++++ 6 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 core/tools/applyToolOverrides.test.ts create mode 100644 core/tools/applyToolOverrides.ts diff --git a/core/config/load.ts b/core/config/load.ts index 35d12d421fc..b570d2047bc 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -109,6 +109,7 @@ const configMergeKeys = { }, slashCommands: (a: any, b: any) => a.name === b.name, customCommands: (a: any, b: any) => a.name === b.name, + tools: (a: any, b: any) => a.name === b.name, }; function loadSerializedConfig( @@ -504,6 +505,7 @@ async function intermediateToFinalConfig({ ...config, contextProviders, tools: getBaseToolDefinitions(), + toolOverrides: config.tools, // Pass through tool overrides from config mcpServerStatuses: [], slashCommands: [], modelsByRole: { diff --git a/core/config/profile/doLoadConfig.ts b/core/config/profile/doLoadConfig.ts index f33c8a1e9f4..cb9dc3595df 100644 --- a/core/config/profile/doLoadConfig.ts +++ b/core/config/profile/doLoadConfig.ts @@ -30,6 +30,7 @@ import { TeamAnalytics } from "../../control-plane/TeamAnalytics.js"; import ContinueProxy from "../../llm/llms/stubs/ContinueProxy"; import { initSlashCommand } from "../../promptFiles/initPrompt"; import { getConfigDependentToolDefinitions } from "../../tools"; +import { applyToolOverrides } from "../../tools/applyToolOverrides"; import { encodeMCPToolUri } from "../../tools/callTool"; import { getMCPToolName } from "../../tools/mcpToolName"; import { GlobalContext } from "../../util/GlobalContext"; @@ -309,6 +310,18 @@ export default async function doLoadConfig(options: { }), ); + // Apply tool overrides from config (e.g., .continuerc.json) + if (newConfig.toolOverrides?.length) { + const toolOverridesResult = applyToolOverrides( + newConfig.tools, + newConfig.toolOverrides, + ); + newConfig.tools = toolOverridesResult.tools; + errors.push(...toolOverridesResult.errors); + // Clear toolOverrides after applying (not needed at runtime) + delete newConfig.toolOverrides; + } + // Detect duplicate tool names const counts: Record = {}; newConfig.tools.forEach((tool) => { diff --git a/core/config/types.ts b/core/config/types.ts index d9f58aca6e2..9a2a3ad1f4c 100644 --- a/core/config/types.ts +++ b/core/config/types.ts @@ -1133,7 +1133,31 @@ declare global { url?: string; clientKey?: string; } - + + /** + * Configuration for overriding built-in tool prompts. + * Allows customization of tool descriptions and behavior at the repo level. + */ + export interface ToolOverride { + /** Tool name to override (matches function.name, e.g., "read_file", "run_terminal_command") */ + name: string; + /** Override the tool's description shown to the LLM */ + description?: string; + /** Override the display title shown in UI */ + displayTitle?: string; + /** Override the action phrases */ + wouldLikeTo?: string; + isCurrently?: string; + hasAlready?: string; + /** Override system message description for non-native tool calling */ + systemMessageDescription?: { + prefix?: string; + exampleArgs?: Array<[string, string | number]>; + }; + /** Set to true to disable this tool */ + disabled?: boolean; + } + // config.json export interface SerializedContinueConfig { env?: string[]; @@ -1156,8 +1180,10 @@ declare global { experimental?: ExperimentalConfig; analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; + /** Tool overrides for customizing built-in tool prompts at the repo level */ + tools?: ToolOverride[]; } - + export type ConfigMergeType = "merge" | "overwrite"; export type ContinueRcJson = Partial & { @@ -1208,8 +1234,10 @@ declare global { experimental?: ExperimentalConfig; /** Analytics configuration */ analytics?: AnalyticsConfig; + /** Tool overrides for customizing built-in tool prompts */ + tools?: ToolOverride[]; } - + // in the actual Continue source code export interface ContinueConfig { allowAnonymousTelemetry?: boolean; @@ -1231,8 +1259,10 @@ declare global { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; tools: Tool[]; + /** Tool overrides from config, applied after all tools are loaded */ + toolOverrides?: ToolOverride[]; } - + export interface BrowserSerializedContinueConfig { allowAnonymousTelemetry?: boolean; models: ModelDescription[]; diff --git a/core/index.d.ts b/core/index.d.ts index 197b5516d0c..4fa6efefa90 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1139,6 +1139,30 @@ export interface Tool { ) => ToolPolicy; } +/** + * Configuration for overriding built-in tool prompts. + * Allows customization of tool descriptions and behavior at the repo level. + */ +export interface ToolOverride { + /** Tool name to override (matches function.name, e.g., "read_file", "run_terminal_command") */ + name: string; + /** Override the tool's description shown to the LLM */ + description?: string; + /** Override the display title shown in UI */ + displayTitle?: string; + /** Override the action phrases */ + wouldLikeTo?: string; + isCurrently?: string; + hasAlready?: string; + /** Override system message description for non-native tool calling */ + systemMessageDescription?: { + prefix?: string; + exampleArgs?: Array<[string, string | number]>; + }; + /** Set to true to disable this tool */ + disabled?: boolean; +} + interface ToolChoice { type: "function"; function: { @@ -1721,6 +1745,8 @@ export interface SerializedContinueConfig { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; data?: DataDestination[]; + /** Tool overrides for customizing built-in tool prompts at the repo level */ + tools?: ToolOverride[]; } export type ConfigMergeType = "merge" | "overwrite"; @@ -1775,6 +1801,8 @@ export interface Config { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; data?: DataDestination[]; + /** Tool overrides for customizing built-in tool prompts */ + tools?: ToolOverride[]; } // in the actual Continue source code @@ -1794,6 +1822,8 @@ export interface ContinueConfig { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; tools: Tool[]; + /** Tool overrides from config, applied after all tools are loaded */ + toolOverrides?: ToolOverride[]; mcpServerStatuses: MCPServerStatus[]; rules: RuleWithSource[]; modelsByRole: Record; diff --git a/core/tools/applyToolOverrides.test.ts b/core/tools/applyToolOverrides.test.ts new file mode 100644 index 00000000000..a0834f9857b --- /dev/null +++ b/core/tools/applyToolOverrides.test.ts @@ -0,0 +1,184 @@ +import { Tool, ToolOverride } from ".."; +import { applyToolOverrides } from "./applyToolOverrides"; + +const mockTool = (name: string, description: string): Tool => ({ + type: "function", + displayTitle: name, + readonly: true, + group: "test", + function: { name, description }, +}); + +describe("applyToolOverrides", () => { + it("should return tools unchanged when no overrides provided", () => { + const tools = [mockTool("read_file", "Read a file")]; + const result = applyToolOverrides(tools, undefined); + expect(result.tools).toEqual(tools); + expect(result.errors).toHaveLength(0); + }); + + it("should return tools unchanged when empty overrides array provided", () => { + const tools = [mockTool("read_file", "Read a file")]; + const result = applyToolOverrides(tools, []); + expect(result.tools).toEqual(tools); + expect(result.errors).toHaveLength(0); + }); + + it("should override description when specified", () => { + const tools = [mockTool("read_file", "Original description")]; + const overrides: ToolOverride[] = [ + { name: "read_file", description: "New description" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].function.description).toBe("New description"); + expect(result.errors).toHaveLength(0); + }); + + it("should override displayTitle when specified", () => { + const tools = [mockTool("read_file", "Read a file")]; + const overrides: ToolOverride[] = [ + { name: "read_file", displayTitle: "Custom Read File" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].displayTitle).toBe("Custom Read File"); + }); + + it("should override action phrases when specified", () => { + const tools = [mockTool("read_file", "Read a file")]; + tools[0].wouldLikeTo = "read {{{ filepath }}}"; + tools[0].isCurrently = "reading {{{ filepath }}}"; + tools[0].hasAlready = "read {{{ filepath }}}"; + + const overrides: ToolOverride[] = [ + { + name: "read_file", + wouldLikeTo: "open {{{ filepath }}}", + isCurrently: "opening {{{ filepath }}}", + hasAlready: "opened {{{ filepath }}}", + }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].wouldLikeTo).toBe("open {{{ filepath }}}"); + expect(result.tools[0].isCurrently).toBe("opening {{{ filepath }}}"); + expect(result.tools[0].hasAlready).toBe("opened {{{ filepath }}}"); + }); + + it("should disable tools when disabled: true", () => { + const tools = [ + mockTool("read_file", "Read"), + mockTool("write_file", "Write"), + ]; + const overrides: ToolOverride[] = [{ name: "read_file", disabled: true }]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools).toHaveLength(1); + expect(result.tools[0].function.name).toBe("write_file"); + expect(result.errors).toHaveLength(0); + }); + + it("should warn when override references unknown tool", () => { + const tools = [mockTool("read_file", "Read")]; + const overrides: ToolOverride[] = [ + { name: "unknown_tool", description: "test" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools).toHaveLength(1); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].message).toContain("unknown_tool"); + expect(result.errors[0].fatal).toBe(false); + }); + + it("should preserve unmodified fields", () => { + const tools = [mockTool("read_file", "Original")]; + tools[0].readonly = true; + tools[0].group = "Built-In"; + + const overrides: ToolOverride[] = [ + { name: "read_file", description: "New description" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].readonly).toBe(true); + expect(result.tools[0].group).toBe("Built-In"); + expect(result.tools[0].displayTitle).toBe("read_file"); + }); + + it("should override systemMessageDescription", () => { + const tools = [mockTool("read_file", "Read")]; + tools[0].systemMessageDescription = { + prefix: "old prefix", + exampleArgs: [["filepath", "/old/path"]], + }; + + const overrides: ToolOverride[] = [ + { + name: "read_file", + systemMessageDescription: { + prefix: "new prefix", + exampleArgs: [["filepath", "/new/path"]], + }, + }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix"); + expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([ + ["filepath", "/new/path"], + ]); + }); + + it("should partially override systemMessageDescription", () => { + const tools = [mockTool("read_file", "Read")]; + tools[0].systemMessageDescription = { + prefix: "old prefix", + exampleArgs: [["filepath", "/old/path"]], + }; + + const overrides: ToolOverride[] = [ + { + name: "read_file", + systemMessageDescription: { + prefix: "new prefix", + // exampleArgs not specified - should preserve original + }, + }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix"); + expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([ + ["filepath", "/old/path"], + ]); + }); + + it("should apply multiple overrides", () => { + const tools = [ + mockTool("read_file", "Read"), + mockTool("write_file", "Write"), + mockTool("delete_file", "Delete"), + ]; + + const overrides: ToolOverride[] = [ + { name: "read_file", description: "Custom read" }, + { name: "write_file", disabled: true }, + { name: "delete_file", displayTitle: "Remove File" }, + ]; + + const result = applyToolOverrides(tools, overrides); + expect(result.tools).toHaveLength(2); + expect(result.tools[0].function.description).toBe("Custom read"); + expect(result.tools[1].displayTitle).toBe("Remove File"); + expect(result.errors).toHaveLength(0); + }); + + it("should not mutate original tools array", () => { + const tools = [mockTool("read_file", "Original")]; + const originalDescription = tools[0].function.description; + + const overrides: ToolOverride[] = [ + { name: "read_file", description: "New description" }, + ]; + const result = applyToolOverrides(tools, overrides); + + // Original should be unchanged + expect(tools[0].function.description).toBe(originalDescription); + // Result should have new description + expect(result.tools[0].function.description).toBe("New description"); + }); +}); diff --git a/core/tools/applyToolOverrides.ts b/core/tools/applyToolOverrides.ts new file mode 100644 index 00000000000..52c7b94cb1f --- /dev/null +++ b/core/tools/applyToolOverrides.ts @@ -0,0 +1,69 @@ +import { ConfigValidationError } from "@continuedev/config-yaml"; +import { Tool, ToolOverride } from ".."; + +export interface ApplyToolOverridesResult { + tools: Tool[]; + errors: ConfigValidationError[]; +} + +/** + * Applies tool overrides from config to the list of tools. + * Overrides can modify tool descriptions, display titles, action phrases, + * system message descriptions, or disable tools entirely. + */ +export function applyToolOverrides( + tools: Tool[], + overrides: ToolOverride[] | undefined, +): ApplyToolOverridesResult { + if (!overrides?.length) { + return { tools, errors: [] }; + } + + const errors: ConfigValidationError[] = []; + const toolsByName = new Map(tools.map((t) => [t.function.name, t])); + + for (const override of overrides) { + const tool = toolsByName.get(override.name); + + if (!tool) { + errors.push({ + fatal: false, + message: `Tool override "${override.name}" does not match any known tool. Available tools: ${Array.from(toolsByName.keys()).join(", ")}`, + }); + continue; + } + + if (override.disabled) { + toolsByName.delete(override.name); + continue; + } + + const updatedTool: Tool = { + ...tool, + function: { + ...tool.function, + description: override.description ?? tool.function.description, + }, + displayTitle: override.displayTitle ?? tool.displayTitle, + wouldLikeTo: override.wouldLikeTo ?? tool.wouldLikeTo, + isCurrently: override.isCurrently ?? tool.isCurrently, + hasAlready: override.hasAlready ?? tool.hasAlready, + }; + + if (override.systemMessageDescription) { + updatedTool.systemMessageDescription = { + prefix: + override.systemMessageDescription.prefix ?? + tool.systemMessageDescription?.prefix ?? + "", + exampleArgs: + override.systemMessageDescription.exampleArgs ?? + tool.systemMessageDescription?.exampleArgs, + }; + } + + toolsByName.set(override.name, updatedTool); + } + + return { tools: Array.from(toolsByName.values()), errors }; +} From 12176f1424618b836e6a5f0bbb222e3e6c15c135 Mon Sep 17 00:00:00 2001 From: shanevcantwell <153727980+shanevcantwell@users.noreply.github.com> Date: Wed, 24 Dec 2025 23:04:24 -0700 Subject: [PATCH 2/3] fix: enable .continuerc.json tool overrides for both config paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #9312, Fixes #9313 Two bugs were preventing workspace .continuerc.json files from working: 1. loadRcConfigs.ts: `rcFiles.map(ide.readFile)` lost `this` binding, causing silent failures when reading .continuerc.json files. Fixed by using `rcFiles.map((uri) => ide.readFile(uri))`. 2. doLoadConfig.ts: YAML config path never loaded workspace configs. Added conditional loading of workspace .continuerc.json files for tool overrides when using config.yaml. Debugging notes: - Added file-based tracing to /tmp/continue_debug.txt in installed extension to trace config loading path - Discovered JSON path was taken but getWorkspaceRcConfigs returned [] - Found listDir found the file but readFile failed silently - Wrapping readFile in try-catch revealed the this binding issue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- core/config/json/loadRcConfigs.ts | 2 +- core/config/profile/doLoadConfig.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/config/json/loadRcConfigs.ts b/core/config/json/loadRcConfigs.ts index ceb7de77b54..cf73eb4aae3 100644 --- a/core/config/json/loadRcConfigs.ts +++ b/core/config/json/loadRcConfigs.ts @@ -18,7 +18,7 @@ export async function getWorkspaceRcConfigs( entry[0].endsWith(".continuerc.json"), ) .map((entry) => joinPathsToUri(dir, entry[0])); - return await Promise.all(rcFiles.map(ide.readFile)); + return await Promise.all(rcFiles.map((uri) => ide.readFile(uri))); }), ); return rcFiles diff --git a/core/config/profile/doLoadConfig.ts b/core/config/profile/doLoadConfig.ts index cb9dc3595df..1b090234712 100644 --- a/core/config/profile/doLoadConfig.ts +++ b/core/config/profile/doLoadConfig.ts @@ -40,6 +40,7 @@ import { Telemetry } from "../../util/posthog"; import { SentryLogger } from "../../util/sentry/SentryLogger"; import { TTS } from "../../util/tts"; import { getWorkspaceContinueRuleDotFiles } from "../getWorkspaceContinueRuleDotFiles"; +import { getWorkspaceRcConfigs } from "../json/loadRcConfigs"; import { loadContinueConfigFromJson } from "../load"; import { CodebaseRulesCache } from "../markdown/loadCodebaseRules"; import { loadMarkdownRules } from "../markdown/loadMarkdownRules"; @@ -310,6 +311,20 @@ export default async function doLoadConfig(options: { }), ); + // Load workspace .continuerc.json files for tool overrides + // (JSON path loads these earlier during config merge, YAML path doesn't) + if (!newConfig.toolOverrides?.length) { + const workspaceRcConfigs = await getWorkspaceRcConfigs(ide); + for (const rcConfig of workspaceRcConfigs) { + if (rcConfig.tools?.length) { + newConfig.toolOverrides = [ + ...(newConfig.toolOverrides ?? []), + ...rcConfig.tools, + ]; + } + } + } + // Apply tool overrides from config (e.g., .continuerc.json) if (newConfig.toolOverrides?.length) { const toolOverridesResult = applyToolOverrides( From 39303e0a6f6f05a63c94bc1f3582857ae5590126 Mon Sep 17 00:00:00 2001 From: "continue[bot]" Date: Thu, 25 Dec 2025 06:28:01 +0000 Subject: [PATCH 3/3] docs: add tool prompt override documentation for .continuerc.json Document the new tool override feature that allows customizing built-in tool prompts at the workspace level. This includes: - Overriding tool descriptions - Disabling specific tools - Customizing system message prompts - Overriding display titles and action phrases This feature is particularly useful for local models that struggle with default tool prompts. Generated with [Continue](https://continue.dev) Co-Authored-By: Continue Co-authored-by: nate --- docs/customize/deep-dives/configuration.mdx | 82 ++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/docs/customize/deep-dives/configuration.mdx b/docs/customize/deep-dives/configuration.mdx index 694b7543013..88df2822cf7 100644 --- a/docs/customize/deep-dives/configuration.mdx +++ b/docs/customize/deep-dives/configuration.mdx @@ -44,7 +44,87 @@ See the full reference for `config.yaml` [here](/reference). The format of `.continuerc.json` is the same as `config.json`, plus one _additional_ property `mergeBehavior`, which can be set to either "merge" or "overwrite". If set to "merge" (the default), `.continuerc.json` will be applied on top of `config.json` (arrays and objects are merged). If set to "overwrite", then every top-level property of `.continuerc.json` will overwrite that property from `config.json`. -Example +#### Customizing Tool Prompts + +You can customize built-in tool prompts at the workspace level using the `tools` array in `.continuerc.json`. This is especially useful when working with local models that may struggle with Continue's default tool prompts. + + + + Customize how tools are described to the LLM: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "read_file", + "description": "Read file contents. Use relative paths from workspace root." + } + ] + } + ``` + + + + Prevent certain tools from being available: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "view_diff", + "disabled": true + } + ] + } + ``` + + + + Override the system message descriptions for non-native tool calling: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "read_file", + "systemMessageDescription": { + "prefix": "To read a file, use read_file with a relative path:", + "exampleArgs": [["filepath", "./docs/README.md"]] + } + } + ] + } + ``` + + + + Customize the UI display and action phrases: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "read_file", + "displayTitle": "View File", + "wouldLikeTo": "view {{{ filepath }}}", + "isCurrently": "viewing {{{ filepath }}}", + "hasAlready": "viewed {{{ filepath }}}" + } + ] + } + ``` + + + + + Tool names match the built-in function names like `read_file`, `run_terminal_command`, `view_diff`, etc. Tools are merged by name during configuration loading. + + +#### Basic Configuration Example ```json title=".continuerc.json" {