Browse Source

Add logic to prevent auto-approving edits of configuration files (#4667)

* Add logic to prevent auto-approving edits of configuration files

* Fix tests

* Update patterns
Matt Rubens 6 months ago
parent
commit
7d0b22f9e6
55 changed files with 488 additions and 32 deletions
  1. 2 0
      packages/types/src/global-settings.ts
  2. 1 0
      packages/types/src/message.ts
  3. 8 1
      src/core/assistant-message/presentAssistantMessage.ts
  4. 9 1
      src/core/prompts/responses.ts
  5. 103 0
      src/core/protect/RooProtectedController.ts
  6. 141 0
      src/core/protect/__tests__/RooProtectedController.spec.ts
  7. 9 3
      src/core/task/Task.ts
  8. 5 1
      src/core/tools/insertContentTool.ts
  9. 1 0
      src/core/tools/listFilesTool.ts
  10. 15 3
      src/core/tools/multiApplyDiffTool.ts
  11. 9 2
      src/core/tools/searchAndReplaceTool.ts
  12. 5 1
      src/core/tools/writeToFileTool.ts
  13. 3 0
      src/core/webview/ClineProvider.ts
  14. 4 0
      src/core/webview/webviewMessageHandler.ts
  15. 2 0
      src/shared/ExtensionMessage.ts
  16. 1 0
      src/shared/WebviewMessage.ts
  17. 1 0
      src/shared/tools.ts
  18. 56 18
      webview-ui/src/components/chat/ChatRow.tsx
  19. 8 1
      webview-ui/src/components/chat/ChatView.tsx
  20. 17 1
      webview-ui/src/components/settings/AutoApproveSettings.tsx
  21. 3 0
      webview-ui/src/components/settings/SettingsView.tsx
  22. 1 0
      webview-ui/src/i18n/locales/ca/chat.json
  23. 4 0
      webview-ui/src/i18n/locales/ca/settings.json
  24. 1 0
      webview-ui/src/i18n/locales/de/chat.json
  25. 4 0
      webview-ui/src/i18n/locales/de/settings.json
  26. 1 0
      webview-ui/src/i18n/locales/en/chat.json
  27. 4 0
      webview-ui/src/i18n/locales/en/settings.json
  28. 1 0
      webview-ui/src/i18n/locales/es/chat.json
  29. 4 0
      webview-ui/src/i18n/locales/es/settings.json
  30. 1 0
      webview-ui/src/i18n/locales/fr/chat.json
  31. 4 0
      webview-ui/src/i18n/locales/fr/settings.json
  32. 1 0
      webview-ui/src/i18n/locales/hi/chat.json
  33. 4 0
      webview-ui/src/i18n/locales/hi/settings.json
  34. 1 0
      webview-ui/src/i18n/locales/it/chat.json
  35. 4 0
      webview-ui/src/i18n/locales/it/settings.json
  36. 1 0
      webview-ui/src/i18n/locales/ja/chat.json
  37. 4 0
      webview-ui/src/i18n/locales/ja/settings.json
  38. 1 0
      webview-ui/src/i18n/locales/ko/chat.json
  39. 4 0
      webview-ui/src/i18n/locales/ko/settings.json
  40. 1 0
      webview-ui/src/i18n/locales/nl/chat.json
  41. 4 0
      webview-ui/src/i18n/locales/nl/settings.json
  42. 1 0
      webview-ui/src/i18n/locales/pl/chat.json
  43. 4 0
      webview-ui/src/i18n/locales/pl/settings.json
  44. 1 0
      webview-ui/src/i18n/locales/pt-BR/chat.json
  45. 4 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  46. 1 0
      webview-ui/src/i18n/locales/ru/chat.json
  47. 4 0
      webview-ui/src/i18n/locales/ru/settings.json
  48. 1 0
      webview-ui/src/i18n/locales/tr/chat.json
  49. 4 0
      webview-ui/src/i18n/locales/tr/settings.json
  50. 1 0
      webview-ui/src/i18n/locales/vi/chat.json
  51. 4 0
      webview-ui/src/i18n/locales/vi/settings.json
  52. 1 0
      webview-ui/src/i18n/locales/zh-CN/chat.json
  53. 4 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  54. 1 0
      webview-ui/src/i18n/locales/zh-TW/chat.json
  55. 4 0
      webview-ui/src/i18n/locales/zh-TW/settings.json

+ 2 - 0
packages/types/src/global-settings.ts

@@ -36,6 +36,7 @@ export const globalSettingsSchema = z.object({
 	alwaysAllowReadOnlyOutsideWorkspace: z.boolean().optional(),
 	alwaysAllowWrite: z.boolean().optional(),
 	alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
+	alwaysAllowWriteProtected: z.boolean().optional(),
 	writeDelayMs: z.number().optional(),
 	alwaysAllowBrowser: z.boolean().optional(),
 	alwaysApproveResubmit: z.boolean().optional(),
@@ -177,6 +178,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
 	alwaysAllowReadOnlyOutsideWorkspace: false,
 	alwaysAllowWrite: true,
 	alwaysAllowWriteOutsideWorkspace: false,
+	alwaysAllowWriteProtected: false,
 	writeDelayMs: 1000,
 	alwaysAllowBrowser: true,
 	alwaysApproveResubmit: true,

+ 1 - 0
packages/types/src/message.ts

@@ -153,6 +153,7 @@ export const clineMessageSchema = z.object({
 	checkpoint: z.record(z.string(), z.unknown()).optional(),
 	progressStatus: toolProgressStatusSchema.optional(),
 	contextCondense: contextCondenseSchema.optional(),
+	isProtected: z.boolean().optional(),
 })
 
 export type ClineMessage = z.infer<typeof clineMessageSchema>

+ 8 - 1
src/core/assistant-message/presentAssistantMessage.ts

@@ -261,8 +261,15 @@ export async function presentAssistantMessage(cline: Task) {
 				type: ClineAsk,
 				partialMessage?: string,
 				progressStatus?: ToolProgressStatus,
+				isProtected?: boolean,
 			) => {
-				const { response, text, images } = await cline.ask(type, partialMessage, false, progressStatus)
+				const { response, text, images } = await cline.ask(
+					type,
+					partialMessage,
+					false,
+					progressStatus,
+					isProtected || false,
+				)
 
 				if (response !== "yesButtonClicked") {
 					// Handle both messageResponse and noButtonClicked with text.

+ 9 - 1
src/core/prompts/responses.ts

@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
 import * as path from "path"
 import * as diff from "diff"
 import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/RooIgnoreController"
+import { RooProtectedController } from "../protect/RooProtectedController"
 
 export const formatResponse = {
 	toolDenied: () => `The user denied this operation.`,
@@ -95,6 +96,7 @@ Otherwise, if you have not completed the task and do not need additional informa
 		didHitLimit: boolean,
 		rooIgnoreController: RooIgnoreController | undefined,
 		showRooIgnoredFiles: boolean,
+		rooProtectedController?: RooProtectedController,
 	): string => {
 		const sorted = files
 			.map((file) => {
@@ -143,7 +145,13 @@ Otherwise, if you have not completed the task and do not need additional informa
 					// Otherwise, mark it with a lock symbol
 					rooIgnoreParsed.push(LOCK_TEXT_SYMBOL + " " + filePath)
 				} else {
-					rooIgnoreParsed.push(filePath)
+					// Check if file is write-protected (only for non-ignored files)
+					const isWriteProtected = rooProtectedController?.isWriteProtected(absoluteFilePath) || false
+					if (isWriteProtected) {
+						rooIgnoreParsed.push("🛡️ " + filePath)
+					} else {
+						rooIgnoreParsed.push(filePath)
+					}
 				}
 			}
 		}

+ 103 - 0
src/core/protect/RooProtectedController.ts

@@ -0,0 +1,103 @@
+import path from "path"
+import ignore, { Ignore } from "ignore"
+
+export const SHIELD_SYMBOL = "\u{1F6E1}"
+
+/**
+ * Controls write access to Roo configuration files by enforcing protection patterns.
+ * Prevents auto-approved modifications to sensitive Roo configuration files.
+ */
+export class RooProtectedController {
+	private cwd: string
+	private ignoreInstance: Ignore
+
+	// Predefined list of protected Roo configuration patterns
+	private static readonly PROTECTED_PATTERNS = [
+		".rooignore",
+		".roomodes",
+		".roorules*",
+		".clinerules*",
+		".roo/**",
+		".rooprotected", // For future use
+	]
+
+	constructor(cwd: string) {
+		this.cwd = cwd
+		// Initialize ignore instance with protected patterns
+		this.ignoreInstance = ignore()
+		this.ignoreInstance.add(RooProtectedController.PROTECTED_PATTERNS)
+	}
+
+	/**
+	 * Check if a file is write-protected
+	 * @param filePath - Path to check (relative to cwd)
+	 * @returns true if file is write-protected, false otherwise
+	 */
+	isWriteProtected(filePath: string): boolean {
+		try {
+			// Normalize path to be relative to cwd and use forward slashes
+			const absolutePath = path.resolve(this.cwd, filePath)
+			const relativePath = path.relative(this.cwd, absolutePath).toPosix()
+
+			// Use ignore library to check if file matches any protected pattern
+			return this.ignoreInstance.ignores(relativePath)
+		} catch (error) {
+			// If there's an error processing the path, err on the side of caution
+			// Ignore is designed to work with relative file paths, so will throw error for paths outside cwd
+			console.error(`Error checking protection for ${filePath}:`, error)
+			return false
+		}
+	}
+
+	/**
+	 * Get set of write-protected files from a list
+	 * @param paths - Array of paths to filter (relative to cwd)
+	 * @returns Set of protected file paths
+	 */
+	getProtectedFiles(paths: string[]): Set<string> {
+		const protectedFiles = new Set<string>()
+
+		for (const filePath of paths) {
+			if (this.isWriteProtected(filePath)) {
+				protectedFiles.add(filePath)
+			}
+		}
+
+		return protectedFiles
+	}
+
+	/**
+	 * Filter an array of paths, marking which ones are protected
+	 * @param paths - Array of paths to check (relative to cwd)
+	 * @returns Array of objects with path and protection status
+	 */
+	annotatePathsWithProtection(paths: string[]): Array<{ path: string; isProtected: boolean }> {
+		return paths.map((filePath) => ({
+			path: filePath,
+			isProtected: this.isWriteProtected(filePath),
+		}))
+	}
+
+	/**
+	 * Get display message for protected file operations
+	 */
+	getProtectionMessage(): string {
+		return "This is a Roo configuration file and requires approval for modifications"
+	}
+
+	/**
+	 * Get formatted instructions about protected files for the LLM
+	 * @returns Formatted instructions about file protection
+	 */
+	getInstructions(): string {
+		const patterns = RooProtectedController.PROTECTED_PATTERNS.join(", ")
+		return `# Protected Files\n\n(The following Roo configuration file patterns are write-protected and always require approval for modifications, regardless of autoapproval settings. When using list_files, you'll notice a ${SHIELD_SYMBOL} next to files that are write-protected.)\n\nProtected patterns: ${patterns}`
+	}
+
+	/**
+	 * Get the list of protected patterns (for testing/debugging)
+	 */
+	static getProtectedPatterns(): readonly string[] {
+		return RooProtectedController.PROTECTED_PATTERNS
+	}
+}

+ 141 - 0
src/core/protect/__tests__/RooProtectedController.spec.ts

@@ -0,0 +1,141 @@
+import path from "path"
+import { RooProtectedController } from "../RooProtectedController"
+
+describe("RooProtectedController", () => {
+	const TEST_CWD = "/test/workspace"
+	let controller: RooProtectedController
+
+	beforeEach(() => {
+		controller = new RooProtectedController(TEST_CWD)
+	})
+
+	describe("isWriteProtected", () => {
+		it("should protect .rooignore file", () => {
+			expect(controller.isWriteProtected(".rooignore")).toBe(true)
+		})
+
+		it("should protect files in .roo directory", () => {
+			expect(controller.isWriteProtected(".roo/config.json")).toBe(true)
+			expect(controller.isWriteProtected(".roo/settings/user.json")).toBe(true)
+			expect(controller.isWriteProtected(".roo/modes/custom.json")).toBe(true)
+		})
+
+		it("should protect .rooprotected file", () => {
+			expect(controller.isWriteProtected(".rooprotected")).toBe(true)
+		})
+
+		it("should protect .roomodes files", () => {
+			expect(controller.isWriteProtected(".roomodes")).toBe(true)
+		})
+
+		it("should protect .roorules* files", () => {
+			expect(controller.isWriteProtected(".roorules")).toBe(true)
+			expect(controller.isWriteProtected(".roorules.md")).toBe(true)
+		})
+
+		it("should protect .clinerules* files", () => {
+			expect(controller.isWriteProtected(".clinerules")).toBe(true)
+			expect(controller.isWriteProtected(".clinerules.md")).toBe(true)
+		})
+
+		it("should not protect other files starting with .roo", () => {
+			expect(controller.isWriteProtected(".roosettings")).toBe(false)
+			expect(controller.isWriteProtected(".rooconfig")).toBe(false)
+		})
+
+		it("should not protect regular files", () => {
+			expect(controller.isWriteProtected("src/index.ts")).toBe(false)
+			expect(controller.isWriteProtected("package.json")).toBe(false)
+			expect(controller.isWriteProtected("README.md")).toBe(false)
+		})
+
+		it("should not protect files that contain 'roo' but don't start with .roo", () => {
+			expect(controller.isWriteProtected("src/roo-utils.ts")).toBe(false)
+			expect(controller.isWriteProtected("config/roo.config.js")).toBe(false)
+		})
+
+		it("should handle nested paths correctly", () => {
+			expect(controller.isWriteProtected(".roo/config.json")).toBe(true) // .roo/** matches at root
+			expect(controller.isWriteProtected("nested/.rooignore")).toBe(true) // .rooignore matches anywhere by default
+			expect(controller.isWriteProtected("nested/.roomodes")).toBe(true) // .roomodes matches anywhere by default
+			expect(controller.isWriteProtected("nested/.roorules.md")).toBe(true) // .roorules* matches anywhere by default
+		})
+
+		it("should handle absolute paths by converting to relative", () => {
+			const absolutePath = path.join(TEST_CWD, ".rooignore")
+			expect(controller.isWriteProtected(absolutePath)).toBe(true)
+		})
+
+		it("should handle paths with different separators", () => {
+			expect(controller.isWriteProtected(".roo\\config.json")).toBe(true)
+			expect(controller.isWriteProtected(".roo/config.json")).toBe(true)
+		})
+	})
+
+	describe("getProtectedFiles", () => {
+		it("should return set of protected files from a list", () => {
+			const files = ["src/index.ts", ".rooignore", "package.json", ".roo/config.json", "README.md"]
+
+			const protectedFiles = controller.getProtectedFiles(files)
+
+			expect(protectedFiles).toEqual(new Set([".rooignore", ".roo/config.json"]))
+		})
+
+		it("should return empty set when no files are protected", () => {
+			const files = ["src/index.ts", "package.json", "README.md"]
+
+			const protectedFiles = controller.getProtectedFiles(files)
+
+			expect(protectedFiles).toEqual(new Set())
+		})
+	})
+
+	describe("annotatePathsWithProtection", () => {
+		it("should annotate paths with protection status", () => {
+			const files = ["src/index.ts", ".rooignore", ".roo/config.json", "package.json"]
+
+			const annotated = controller.annotatePathsWithProtection(files)
+
+			expect(annotated).toEqual([
+				{ path: "src/index.ts", isProtected: false },
+				{ path: ".rooignore", isProtected: true },
+				{ path: ".roo/config.json", isProtected: true },
+				{ path: "package.json", isProtected: false },
+			])
+		})
+	})
+
+	describe("getProtectionMessage", () => {
+		it("should return appropriate protection message", () => {
+			const message = controller.getProtectionMessage()
+			expect(message).toBe("This is a Roo configuration file and requires approval for modifications")
+		})
+	})
+
+	describe("getInstructions", () => {
+		it("should return formatted instructions about protected files", () => {
+			const instructions = controller.getInstructions()
+
+			expect(instructions).toContain("# Protected Files")
+			expect(instructions).toContain("write-protected")
+			expect(instructions).toContain(".rooignore")
+			expect(instructions).toContain(".roo/**")
+			expect(instructions).toContain("\u{1F6E1}") // Shield symbol
+		})
+	})
+
+	describe("getProtectedPatterns", () => {
+		it("should return the list of protected patterns", () => {
+			const patterns = RooProtectedController.getProtectedPatterns()
+
+			expect(patterns).toEqual([
+				".rooignore",
+				".roomodes",
+				".roorules*",
+				".clinerules*",
+				".roo/**",
+				".rooprotected",
+			])
+		})
+	})
+})

+ 9 - 3
src/core/task/Task.ts

@@ -65,6 +65,7 @@ import { SYSTEM_PROMPT } from "../prompts/system"
 import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector"
 import { FileContextTracker } from "../context-tracking/FileContextTracker"
 import { RooIgnoreController } from "../ignore/RooIgnoreController"
+import { RooProtectedController } from "../protect/RooProtectedController"
 import { type AssistantMessageContent, parseAssistantMessage, presentAssistantMessage } from "../assistant-message"
 import { truncateConversationIfNeeded } from "../sliding-window"
 import { ClineProvider } from "../webview/ClineProvider"
@@ -144,6 +145,7 @@ export class Task extends EventEmitter<ClineEvents> {
 
 	toolRepetitionDetector: ToolRepetitionDetector
 	rooIgnoreController?: RooIgnoreController
+	rooProtectedController?: RooProtectedController
 	fileContextTracker: FileContextTracker
 	urlContentFetcher: UrlContentFetcher
 	terminalProcess?: RooTerminalProcess
@@ -223,6 +225,7 @@ export class Task extends EventEmitter<ClineEvents> {
 		this.taskNumber = -1
 
 		this.rooIgnoreController = new RooIgnoreController(this.cwd)
+		this.rooProtectedController = new RooProtectedController(this.cwd)
 		this.fileContextTracker = new FileContextTracker(provider, this.taskId)
 
 		this.rooIgnoreController.initialize().catch((error) => {
@@ -406,6 +409,7 @@ export class Task extends EventEmitter<ClineEvents> {
 		text?: string,
 		partial?: boolean,
 		progressStatus?: ToolProgressStatus,
+		isProtected?: boolean,
 	): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
 		// If this Cline instance was aborted by the provider, then the only
 		// thing keeping us alive is a promise still running in the background,
@@ -433,6 +437,7 @@ export class Task extends EventEmitter<ClineEvents> {
 					lastMessage.text = text
 					lastMessage.partial = partial
 					lastMessage.progressStatus = progressStatus
+					lastMessage.isProtected = isProtected
 					// TODO: Be more efficient about saving and posting only new
 					// data or one whole message at a time so ignore partial for
 					// saves, and only post parts of partial message instead of
@@ -444,7 +449,7 @@ export class Task extends EventEmitter<ClineEvents> {
 					// state.
 					askTs = Date.now()
 					this.lastMessageTs = askTs
-					await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
+					await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial, isProtected })
 					throw new Error("Current ask promise was ignored (#2)")
 				}
 			} else {
@@ -471,6 +476,7 @@ export class Task extends EventEmitter<ClineEvents> {
 					lastMessage.text = text
 					lastMessage.partial = false
 					lastMessage.progressStatus = progressStatus
+					lastMessage.isProtected = isProtected
 					await this.saveClineMessages()
 					this.updateClineMessage(lastMessage)
 				} else {
@@ -480,7 +486,7 @@ export class Task extends EventEmitter<ClineEvents> {
 					this.askResponseImages = undefined
 					askTs = Date.now()
 					this.lastMessageTs = askTs
-					await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
+					await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
 				}
 			}
 		} else {
@@ -490,7 +496,7 @@ export class Task extends EventEmitter<ClineEvents> {
 			this.askResponseImages = undefined
 			askTs = Date.now()
 			this.lastMessageTs = askTs
-			await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
+			await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
 		}
 
 		await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })

+ 5 - 1
src/core/tools/insertContentTool.ts

@@ -66,6 +66,9 @@ export async function insertContentTool(
 			return
 		}
 
+		// Check if file is write-protected
+		const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
+
 		const absolutePath = path.resolve(cline.cwd, relPath)
 		const fileExists = await fileExistsAtPath(absolutePath)
 
@@ -124,10 +127,11 @@ export async function insertContentTool(
 			...sharedMessageProps,
 			diff,
 			lineNumber: lineNumber,
+			isProtected: isWriteProtected,
 		} satisfies ClineSayTool)
 
 		const didApprove = await cline
-			.ask("tool", completeMessage, false)
+			.ask("tool", completeMessage, isWriteProtected)
 			.then((response) => response.response === "yesButtonClicked")
 
 		if (!didApprove) {

+ 1 - 0
src/core/tools/listFilesTool.ts

@@ -64,6 +64,7 @@ export async function listFilesTool(
 				didHitLimit,
 				cline.rooIgnoreController,
 				showRooIgnoredFiles,
+				cline.rooProtectedController,
 			)
 
 			const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result } satisfies ClineSayTool)

+ 15 - 3
src/core/tools/multiApplyDiffTool.ts

@@ -161,7 +161,6 @@ Expected structure:
 Original error: ${errorMessage}`
 			throw new Error(detailedError)
 		}
-
 	} else if (legacyPath && typeof legacyDiffContent === "string") {
 		// Handle legacy parameters (old way)
 		usingLegacyParams = true
@@ -236,6 +235,9 @@ Original error: ${errorMessage}`
 				continue
 			}
 
+			// Check if file is write-protected
+			const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
+
 			// Verify file exists
 			const absolutePath = path.resolve(cline.cwd, relPath)
 			const fileExists = await fileExistsAtPath(absolutePath)
@@ -258,6 +260,11 @@ Original error: ${errorMessage}`
 
 		// Handle batch approval if there are multiple files
 		if (operationsToApprove.length > 1) {
+			// Check if any files are write-protected
+			const hasProtectedFiles = operationsToApprove.some(
+				(opResult) => cline.rooProtectedController?.isWriteProtected(opResult.path) || false,
+			)
+
 			// Prepare batch diff data
 			const batchDiffs = operationsToApprove.map((opResult) => {
 				const readablePath = getReadablePath(cline.cwd, opResult.path)
@@ -279,9 +286,10 @@ Original error: ${errorMessage}`
 			const completeMessage = JSON.stringify({
 				tool: "appliedDiff",
 				batchDiffs,
+				isProtected: hasProtectedFiles,
 			} satisfies ClineSayTool)
 
-			const { response, text, images } = await cline.ask("tool", completeMessage, false)
+			const { response, text, images } = await cline.ask("tool", completeMessage, hasProtectedFiles)
 
 			// Process batch response
 			if (response === "yesButtonClicked") {
@@ -485,9 +493,11 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
 				await cline.diffViewProvider.scrollToFirstDiff()
 
 				// For batch operations, we've already gotten approval
+				const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
 				const sharedMessageProps: ClineSayTool = {
 					tool: "appliedDiff",
 					path: getReadablePath(cline.cwd, relPath),
+					isProtected: isWriteProtected,
 				}
 
 				// If single file, ask for approval
@@ -511,7 +521,9 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
 						)
 					}
 
-					didApprove = await askApproval("tool", operationMessage, toolProgressStatus)
+					// Check if file is write-protected
+					const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
+					didApprove = await askApproval("tool", operationMessage, toolProgressStatus, isWriteProtected)
 				}
 
 				if (!didApprove) {

+ 9 - 2
src/core/tools/searchAndReplaceTool.ts

@@ -123,6 +123,9 @@ export async function searchAndReplaceTool(
 			return
 		}
 
+		// Check if file is write-protected
+		const isWriteProtected = cline.rooProtectedController?.isWriteProtected(validRelPath) || false
+
 		const absolutePath = path.resolve(cline.cwd, validRelPath)
 		const fileExists = await fileExistsAtPath(absolutePath)
 
@@ -207,9 +210,13 @@ export async function searchAndReplaceTool(
 		await cline.diffViewProvider.update(newContent, true)
 
 		// Request user approval for changes
-		const completeMessage = JSON.stringify({ ...sharedMessageProps, diff } satisfies ClineSayTool)
+		const completeMessage = JSON.stringify({
+			...sharedMessageProps,
+			diff,
+			isProtected: isWriteProtected,
+		} satisfies ClineSayTool)
 		const didApprove = await cline
-			.ask("tool", completeMessage, false)
+			.ask("tool", completeMessage, isWriteProtected)
 			.then((response) => response.response === "yesButtonClicked")
 
 		if (!didApprove) {

+ 5 - 1
src/core/tools/writeToFileTool.ts

@@ -56,6 +56,9 @@ export async function writeToFileTool(
 		return
 	}
 
+	// Check if file is write-protected
+	const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
+
 	// Check if file exists using cached map or fs.access
 	let fileExists: boolean
 
@@ -90,6 +93,7 @@ export async function writeToFileTool(
 		path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
 		content: newContent,
 		isOutsideWorkspace,
+		isProtected: isWriteProtected,
 	}
 
 	try {
@@ -201,7 +205,7 @@ export async function writeToFileTool(
 					: undefined,
 			} satisfies ClineSayTool)
 
-			const didApprove = await askApproval("tool", completeMessage)
+			const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
 
 			if (!didApprove) {
 				await cline.diffViewProvider.revertChanges()

+ 3 - 0
src/core/webview/ClineProvider.ts

@@ -1276,6 +1276,7 @@ export class ClineProvider
 			alwaysAllowReadOnlyOutsideWorkspace,
 			alwaysAllowWrite,
 			alwaysAllowWriteOutsideWorkspace,
+			alwaysAllowWriteProtected,
 			alwaysAllowExecute,
 			alwaysAllowBrowser,
 			alwaysAllowMcp,
@@ -1369,6 +1370,7 @@ export class ClineProvider
 			alwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,
 			alwaysAllowWrite: alwaysAllowWrite ?? false,
 			alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
+			alwaysAllowWriteProtected: alwaysAllowWriteProtected ?? false,
 			alwaysAllowExecute: alwaysAllowExecute ?? false,
 			alwaysAllowBrowser: alwaysAllowBrowser ?? false,
 			alwaysAllowMcp: alwaysAllowMcp ?? false,
@@ -1529,6 +1531,7 @@ export class ClineProvider
 			alwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,
 			alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
 			alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
+			alwaysAllowWriteProtected: stateValues.alwaysAllowWriteProtected ?? false,
 			alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
 			alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
 			alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,

+ 4 - 0
src/core/webview/webviewMessageHandler.ts

@@ -155,6 +155,10 @@ export const webviewMessageHandler = async (
 			await updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined)
 			await provider.postStateToWebview()
 			break
+		case "alwaysAllowWriteProtected":
+			await updateGlobalState("alwaysAllowWriteProtected", message.bool ?? undefined)
+			await provider.postStateToWebview()
+			break
 		case "alwaysAllowExecute":
 			await updateGlobalState("alwaysAllowExecute", message.bool ?? undefined)
 			await provider.postStateToWebview()

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -151,6 +151,7 @@ export type ExtensionState = Pick<
 	| "alwaysAllowReadOnlyOutsideWorkspace"
 	| "alwaysAllowWrite"
 	| "alwaysAllowWriteOutsideWorkspace"
+	| "alwaysAllowWriteProtected"
 	// | "writeDelayMs" // Optional in GlobalSettings, required here.
 	| "alwaysAllowBrowser"
 	| "alwaysApproveResubmit"
@@ -276,6 +277,7 @@ export interface ClineSayTool {
 	mode?: string
 	reason?: string
 	isOutsideWorkspace?: boolean
+	isProtected?: boolean
 	additionalFileCount?: number // Number of additional files in the same read_file request
 	search?: string
 	replace?: string

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -29,6 +29,7 @@ export interface WebviewMessage {
 		| "alwaysAllowReadOnlyOutsideWorkspace"
 		| "alwaysAllowWrite"
 		| "alwaysAllowWriteOutsideWorkspace"
+		| "alwaysAllowWriteProtected"
 		| "alwaysAllowExecute"
 		| "webviewDidLaunch"
 		| "newTask"

+ 1 - 0
src/shared/tools.ts

@@ -8,6 +8,7 @@ export type AskApproval = (
 	type: ClineAsk,
 	partialMessage?: string,
 	progressStatus?: ToolProgressStatus,
+	forceApproval?: boolean,
 ) => Promise<boolean>
 
 export type HandleError = (action: string, error: Error) => Promise<void>

+ 56 - 18
webview-ui/src/components/chat/ChatRow.tsx

@@ -313,11 +313,20 @@ export const ChatRowContent = ({
 				return (
 					<>
 						<div style={headerStyle}>
-							{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")}
+							{tool.isProtected ? (
+								<span
+									className="codicon codicon-lock"
+									style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
+								/>
+							) : (
+								toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")
+							)}
 							<span style={{ fontWeight: "bold" }}>
-								{tool.isOutsideWorkspace
-									? t("chat:fileOperations.wantsToEditOutsideWorkspace")
-									: t("chat:fileOperations.wantsToEdit")}
+								{tool.isProtected
+									? t("chat:fileOperations.wantsToEditProtected")
+									: tool.isOutsideWorkspace
+										? t("chat:fileOperations.wantsToEditOutsideWorkspace")
+										: t("chat:fileOperations.wantsToEdit")}
 							</span>
 						</div>
 						<CodeAccordian
@@ -335,15 +344,24 @@ export const ChatRowContent = ({
 				return (
 					<>
 						<div style={headerStyle}>
-							{toolIcon("insert")}
+							{tool.isProtected ? (
+								<span
+									className="codicon codicon-lock"
+									style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
+								/>
+							) : (
+								toolIcon("insert")
+							)}
 							<span style={{ fontWeight: "bold" }}>
-								{tool.isOutsideWorkspace
-									? t("chat:fileOperations.wantsToEditOutsideWorkspace")
-									: tool.lineNumber === 0
-										? t("chat:fileOperations.wantsToInsertAtEnd")
-										: t("chat:fileOperations.wantsToInsertWithLineNumber", {
-												lineNumber: tool.lineNumber,
-											})}
+								{tool.isProtected
+									? t("chat:fileOperations.wantsToEditProtected")
+									: tool.isOutsideWorkspace
+										? t("chat:fileOperations.wantsToEditOutsideWorkspace")
+										: tool.lineNumber === 0
+											? t("chat:fileOperations.wantsToInsertAtEnd")
+											: t("chat:fileOperations.wantsToInsertWithLineNumber", {
+													lineNumber: tool.lineNumber,
+												})}
 							</span>
 						</div>
 						<CodeAccordian
@@ -361,11 +379,20 @@ export const ChatRowContent = ({
 				return (
 					<>
 						<div style={headerStyle}>
-							{toolIcon("replace")}
+							{tool.isProtected ? (
+								<span
+									className="codicon codicon-lock"
+									style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
+								/>
+							) : (
+								toolIcon("replace")
+							)}
 							<span style={{ fontWeight: "bold" }}>
-								{message.type === "ask"
-									? t("chat:fileOperations.wantsToSearchReplace")
-									: t("chat:fileOperations.didSearchReplace")}
+								{tool.isProtected && message.type === "ask"
+									? t("chat:fileOperations.wantsToEditProtected")
+									: message.type === "ask"
+										? t("chat:fileOperations.wantsToSearchReplace")
+										: t("chat:fileOperations.didSearchReplace")}
 							</span>
 						</div>
 						<CodeAccordian
@@ -405,8 +432,19 @@ export const ChatRowContent = ({
 				return (
 					<>
 						<div style={headerStyle}>
-							{toolIcon("new-file")}
-							<span style={{ fontWeight: "bold" }}>{t("chat:fileOperations.wantsToCreate")}</span>
+							{tool.isProtected ? (
+								<span
+									className="codicon codicon-lock"
+									style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
+								/>
+							) : (
+								toolIcon("new-file")
+							)}
+							<span style={{ fontWeight: "bold" }}>
+								{tool.isProtected
+									? t("chat:fileOperations.wantsToEditProtected")
+									: t("chat:fileOperations.wantsToCreate")}
+							</span>
 						</div>
 						<CodeAccordian
 							path={tool.path}

+ 8 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -79,6 +79,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		alwaysAllowReadOnlyOutsideWorkspace,
 		alwaysAllowWrite,
 		alwaysAllowWriteOutsideWorkspace,
+		alwaysAllowWriteProtected,
 		alwaysAllowExecute,
 		alwaysAllowMcp,
 		allowedCommands,
@@ -914,13 +915,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				}
 
 				const isOutsideWorkspace = !!tool.isOutsideWorkspace
+				const isProtected = message.isProtected
 
 				if (isReadOnlyToolAction(message)) {
 					return alwaysAllowReadOnly && (!isOutsideWorkspace || alwaysAllowReadOnlyOutsideWorkspace)
 				}
 
 				if (isWriteToolAction(message)) {
-					return alwaysAllowWrite && (!isOutsideWorkspace || alwaysAllowWriteOutsideWorkspace)
+					return (
+						alwaysAllowWrite &&
+						(!isOutsideWorkspace || alwaysAllowWriteOutsideWorkspace) &&
+						(!isProtected || alwaysAllowWriteProtected)
+					)
 				}
 			}
 
@@ -934,6 +940,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 			isReadOnlyToolAction,
 			alwaysAllowWrite,
 			alwaysAllowWriteOutsideWorkspace,
+			alwaysAllowWriteProtected,
 			isWriteToolAction,
 			alwaysAllowExecute,
 			isAllowedCommand,

+ 17 - 1
webview-ui/src/components/settings/AutoApproveSettings.tsx

@@ -16,6 +16,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 	alwaysAllowReadOnlyOutsideWorkspace?: boolean
 	alwaysAllowWrite?: boolean
 	alwaysAllowWriteOutsideWorkspace?: boolean
+	alwaysAllowWriteProtected?: boolean
 	writeDelayMs: number
 	alwaysAllowBrowser?: boolean
 	alwaysApproveResubmit?: boolean
@@ -30,6 +31,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 		| "alwaysAllowReadOnlyOutsideWorkspace"
 		| "alwaysAllowWrite"
 		| "alwaysAllowWriteOutsideWorkspace"
+		| "alwaysAllowWriteProtected"
 		| "writeDelayMs"
 		| "alwaysAllowBrowser"
 		| "alwaysApproveResubmit"
@@ -47,6 +49,7 @@ export const AutoApproveSettings = ({
 	alwaysAllowReadOnlyOutsideWorkspace,
 	alwaysAllowWrite,
 	alwaysAllowWriteOutsideWorkspace,
+	alwaysAllowWriteProtected,
 	writeDelayMs,
 	alwaysAllowBrowser,
 	alwaysApproveResubmit,
@@ -138,10 +141,23 @@ export const AutoApproveSettings = ({
 									{t("settings:autoApprove.write.outsideWorkspace.label")}
 								</span>
 							</VSCodeCheckbox>
-							<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
+							<div className="text-vscode-descriptionForeground text-sm mt-1">
 								{t("settings:autoApprove.write.outsideWorkspace.description")}
 							</div>
 						</div>
+						<div>
+							<VSCodeCheckbox
+								checked={alwaysAllowWriteProtected}
+								onChange={(e: any) =>
+									setCachedStateField("alwaysAllowWriteProtected", e.target.checked)
+								}
+								data-testid="always-allow-write-protected-checkbox">
+								<span className="font-medium">{t("settings:autoApprove.write.protected.label")}</span>
+							</VSCodeCheckbox>
+							<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
+								{t("settings:autoApprove.write.protected.description")}
+							</div>
+						</div>
 						<div>
 							<div className="flex items-center gap-2">
 								<Slider

+ 3 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -131,6 +131,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 		alwaysAllowSubtasks,
 		alwaysAllowWrite,
 		alwaysAllowWriteOutsideWorkspace,
+		alwaysAllowWriteProtected,
 		alwaysApproveResubmit,
 		autoCondenseContext,
 		autoCondenseContextPercent,
@@ -253,6 +254,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 			})
 			vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
 			vscode.postMessage({ type: "alwaysAllowWriteOutsideWorkspace", bool: alwaysAllowWriteOutsideWorkspace })
+			vscode.postMessage({ type: "alwaysAllowWriteProtected", bool: alwaysAllowWriteProtected })
 			vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
 			vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
 			vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
@@ -571,6 +573,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 							alwaysAllowReadOnlyOutsideWorkspace={alwaysAllowReadOnlyOutsideWorkspace}
 							alwaysAllowWrite={alwaysAllowWrite}
 							alwaysAllowWriteOutsideWorkspace={alwaysAllowWriteOutsideWorkspace}
+							alwaysAllowWriteProtected={alwaysAllowWriteProtected}
 							writeDelayMs={writeDelayMs}
 							alwaysAllowBrowser={alwaysAllowBrowser}
 							alwaysApproveResubmit={alwaysApproveResubmit}

+ 1 - 0
webview-ui/src/i18n/locales/ca/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo ha llegit aquest fitxer:",
 		"wantsToEdit": "Roo vol editar aquest fitxer:",
 		"wantsToEditOutsideWorkspace": "Roo vol editar aquest fitxer fora de l'espai de treball:",
+		"wantsToEditProtected": "Roo vol editar un fitxer de configuració protegit:",
 		"wantsToCreate": "Roo vol crear un nou fitxer:",
 		"wantsToSearchReplace": "Roo vol realitzar cerca i substitució en aquest fitxer:",
 		"didSearchReplace": "Roo ha realitzat cerca i substitució en aquest fitxer:",

+ 4 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Incloure fitxers fora de l'espai de treball",
 				"description": "Permetre a Roo crear i editar fitxers fora de l'espai de treball actual sense requerir aprovació."
+			},
+			"protected": {
+				"label": "Incloure fitxers protegits",
+				"description": "Permetre a Roo crear i editar fitxers protegits (com .rooignore i fitxers de configuració .roo/) sense requerir aprovació."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/de/chat.json

@@ -146,6 +146,7 @@
 		"didRead": "Roo hat diese Datei gelesen:",
 		"wantsToEdit": "Roo möchte diese Datei bearbeiten:",
 		"wantsToEditOutsideWorkspace": "Roo möchte diese Datei außerhalb des Arbeitsbereichs bearbeiten:",
+		"wantsToEditProtected": "Roo möchte eine geschützte Konfigurationsdatei bearbeiten:",
 		"wantsToCreate": "Roo möchte eine neue Datei erstellen:",
 		"wantsToSearchReplace": "Roo möchte in dieser Datei suchen und ersetzen:",
 		"didSearchReplace": "Roo hat Suchen und Ersetzen in dieser Datei durchgeführt:",

+ 4 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Dateien außerhalb des Arbeitsbereichs einbeziehen",
 				"description": "Roo erlauben, Dateien außerhalb des aktuellen Arbeitsbereichs ohne Genehmigung zu erstellen und zu bearbeiten."
+			},
+			"protected": {
+				"label": "Geschützte Dateien einbeziehen",
+				"description": "Roo erlauben, geschützte Dateien (wie .rooignore und .roo/ Konfigurationsdateien) ohne Genehmigung zu erstellen und zu bearbeiten."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/en/chat.json

@@ -156,6 +156,7 @@
 		"didRead": "Roo read this file:",
 		"wantsToEdit": "Roo wants to edit this file:",
 		"wantsToEditOutsideWorkspace": "Roo wants to edit this file outside of the workspace:",
+		"wantsToEditProtected": "Roo wants to edit a protected configuration file:",
 		"wantsToApplyBatchChanges": "Roo wants to apply changes to multiple files:",
 		"wantsToCreate": "Roo wants to create a new file:",
 		"wantsToSearchReplace": "Roo wants to search and replace in this file:",

+ 4 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Include files outside workspace",
 				"description": "Allow Roo to create and edit files outside the current workspace without requiring approval."
+			},
+			"protected": {
+				"label": "Include protected files",
+				"description": "Allow Roo to create and edit protected files (like .rooignore and .roo/ configuration files) without requiring approval."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/es/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo leyó este archivo:",
 		"wantsToEdit": "Roo quiere editar este archivo:",
 		"wantsToEditOutsideWorkspace": "Roo quiere editar este archivo fuera del espacio de trabajo:",
+		"wantsToEditProtected": "Roo quiere editar un archivo de configuración protegido:",
 		"wantsToCreate": "Roo quiere crear un nuevo archivo:",
 		"wantsToSearchReplace": "Roo quiere realizar búsqueda y reemplazo en este archivo:",
 		"didSearchReplace": "Roo realizó búsqueda y reemplazo en este archivo:",

+ 4 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Incluir archivos fuera del espacio de trabajo",
 				"description": "Permitir a Roo crear y editar archivos fuera del espacio de trabajo actual sin requerir aprobación."
+			},
+			"protected": {
+				"label": "Incluir archivos protegidos",
+				"description": "Permitir a Roo crear y editar archivos protegidos (como .rooignore y archivos de configuración .roo/) sin requerir aprobación."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/fr/chat.json

@@ -142,6 +142,7 @@
 		"didRead": "Roo a lu ce fichier :",
 		"wantsToEdit": "Roo veut éditer ce fichier :",
 		"wantsToEditOutsideWorkspace": "Roo veut éditer ce fichier en dehors de l'espace de travail :",
+		"wantsToEditProtected": "Roo veut éditer un fichier de configuration protégé :",
 		"wantsToCreate": "Roo veut créer un nouveau fichier :",
 		"wantsToSearchReplace": "Roo veut effectuer une recherche et remplacement sur ce fichier :",
 		"didSearchReplace": "Roo a effectué une recherche et remplacement sur ce fichier :",

+ 4 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Inclure les fichiers en dehors de l'espace de travail",
 				"description": "Permettre à Roo de créer et modifier des fichiers en dehors de l'espace de travail actuel sans nécessiter d'approbation."
+			},
+			"protected": {
+				"label": "Inclure les fichiers protégés",
+				"description": "Permettre à Roo de créer et modifier des fichiers protégés (comme .rooignore et les fichiers de configuration .roo/) sans nécessiter d'approbation."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/hi/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo ने इस फ़ाइल को पढ़ा:",
 		"wantsToEdit": "Roo इस फ़ाइल को संपादित करना चाहता है:",
 		"wantsToEditOutsideWorkspace": "Roo कार्यक्षेत्र के बाहर इस फ़ाइल को संपादित करना चाहता है:",
+		"wantsToEditProtected": "Roo एक सुरक्षित कॉन्फ़िगरेशन फ़ाइल को संपादित करना चाहता है:",
 		"wantsToCreate": "Roo एक नई फ़ाइल बनाना चाहता है:",
 		"wantsToSearchReplace": "Roo इस फ़ाइल में खोज और प्रतिस्थापन करना चाहता है:",
 		"didSearchReplace": "Roo ने इस फ़ाइल में खोज और प्रतिस्थापन किया:",

+ 4 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "वर्कस्पेस के बाहर की फाइलें शामिल करें",
 				"description": "Roo को अनुमोदन की आवश्यकता के बिना वर्तमान वर्कस्पेस के बाहर फाइलें बनाने और संपादित करने की अनुमति दें।"
+			},
+			"protected": {
+				"label": "संरक्षित फाइलें शामिल करें",
+				"description": "Roo को अनुमोदन की आवश्यकता के बिना संरक्षित फाइलें (.rooignore और .roo/ कॉन्फ़िगरेशन फाइलें जैसी) बनाने और संपादित करने की अनुमति दें।"
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/it/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo ha letto questo file:",
 		"wantsToEdit": "Roo vuole modificare questo file:",
 		"wantsToEditOutsideWorkspace": "Roo vuole modificare questo file al di fuori dell'area di lavoro:",
+		"wantsToEditProtected": "Roo vuole modificare un file di configurazione protetto:",
 		"wantsToCreate": "Roo vuole creare un nuovo file:",
 		"wantsToSearchReplace": "Roo vuole eseguire ricerca e sostituzione in questo file:",
 		"didSearchReplace": "Roo ha eseguito ricerca e sostituzione in questo file:",

+ 4 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Includi file al di fuori dell'area di lavoro",
 				"description": "Permetti a Roo di creare e modificare file al di fuori dell'area di lavoro attuale senza richiedere approvazione."
+			},
+			"protected": {
+				"label": "Includi file protetti",
+				"description": "Permetti a Roo di creare e modificare file protetti (come .rooignore e file di configurazione .roo/) senza richiedere approvazione."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/ja/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Rooはこのファイルを読みました:",
 		"wantsToEdit": "Rooはこのファイルを編集したい:",
 		"wantsToEditOutsideWorkspace": "Rooはワークスペース外のこのファイルを編集したい:",
+		"wantsToEditProtected": "Rooは保護された設定ファイルを編集したい:",
 		"wantsToCreate": "Rooは新しいファイルを作成したい:",
 		"wantsToSearchReplace": "Rooはこのファイルで検索と置換を行う:",
 		"didSearchReplace": "Rooはこのファイルで検索と置換を実行しました:",

+ 4 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "ワークスペース外のファイルを含める",
 				"description": "Rooが承認なしで現在のワークスペース外のファイルを作成・編集することを許可します。"
+			},
+			"protected": {
+				"label": "保護されたファイルを含める",
+				"description": "Rooが保護されたファイル(.rooignoreや.roo/設定ファイルなど)を承認なしで作成・編集することを許可します。"
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/ko/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo가 이 파일을 읽었습니다:",
 		"wantsToEdit": "Roo가 이 파일을 편집하고 싶어합니다:",
 		"wantsToEditOutsideWorkspace": "Roo가 워크스페이스 외부의 이 파일을 편집하고 싶어합니다:",
+		"wantsToEditProtected": "Roo가 보호된 설정 파일을 편집하고 싶어합니다:",
 		"wantsToCreate": "Roo가 새 파일을 만들고 싶어합니다:",
 		"wantsToSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행하고 싶어합니다:",
 		"didSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행했습니다:",

+ 4 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "워크스페이스 외부 파일 포함",
 				"description": "Roo가 승인 없이 현재 워크스페이스 외부의 파일을 생성하고 편집할 수 있도록 허용합니다."
+			},
+			"protected": {
+				"label": "보호된 파일 포함",
+				"description": "Roo가 보호된 파일(.rooignore 및 .roo/ 구성 파일 등)을 승인 없이 생성하고 편집할 수 있도록 허용합니다."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/nl/chat.json

@@ -140,6 +140,7 @@
 		"didRead": "Roo heeft dit bestand gelezen:",
 		"wantsToEdit": "Roo wil dit bestand bewerken:",
 		"wantsToEditOutsideWorkspace": "Roo wil dit bestand buiten de werkruimte bewerken:",
+		"wantsToEditProtected": "Roo wil een beveiligd configuratiebestand bewerken:",
 		"wantsToCreate": "Roo wil een nieuw bestand aanmaken:",
 		"wantsToSearchReplace": "Roo wil zoeken en vervangen in dit bestand:",
 		"didSearchReplace": "Roo heeft zoeken en vervangen uitgevoerd op dit bestand:",

+ 4 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Inclusief bestanden buiten werkruimte",
 				"description": "Sta Roo toe om bestanden buiten de huidige werkruimte aan te maken en te bewerken zonder goedkeuring."
+			},
+			"protected": {
+				"label": "Inclusief beschermde bestanden",
+				"description": "Sta Roo toe om beschermde bestanden (zoals .rooignore en .roo/ configuratiebestanden) aan te maken en te bewerken zonder goedkeuring."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/pl/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo przeczytał ten plik:",
 		"wantsToEdit": "Roo chce edytować ten plik:",
 		"wantsToEditOutsideWorkspace": "Roo chce edytować ten plik poza obszarem roboczym:",
+		"wantsToEditProtected": "Roo chce edytować chroniony plik konfiguracyjny:",
 		"wantsToCreate": "Roo chce utworzyć nowy plik:",
 		"wantsToSearchReplace": "Roo chce wykonać wyszukiwanie i zamianę w tym pliku:",
 		"didSearchReplace": "Roo wykonał wyszukiwanie i zamianę w tym pliku:",

+ 4 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Uwzględnij pliki poza obszarem roboczym",
 				"description": "Pozwól Roo na tworzenie i edycję plików poza bieżącym obszarem roboczym bez konieczności zatwierdzania."
+			},
+			"protected": {
+				"label": "Uwzględnij pliki chronione",
+				"description": "Pozwól Roo na tworzenie i edycję plików chronionych (takich jak .rooignore i pliki konfiguracyjne .roo/) bez konieczności zatwierdzania."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo leu este arquivo:",
 		"wantsToEdit": "Roo quer editar este arquivo:",
 		"wantsToEditOutsideWorkspace": "Roo quer editar este arquivo fora do espaço de trabalho:",
+		"wantsToEditProtected": "Roo quer editar um arquivo de configuração protegido:",
 		"wantsToCreate": "Roo quer criar um novo arquivo:",
 		"wantsToSearchReplace": "Roo quer realizar busca e substituição neste arquivo:",
 		"didSearchReplace": "Roo realizou busca e substituição neste arquivo:",

+ 4 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Incluir arquivos fora do espaço de trabalho",
 				"description": "Permitir que o Roo crie e edite arquivos fora do espaço de trabalho atual sem exigir aprovação."
+			},
+			"protected": {
+				"label": "Incluir arquivos protegidos",
+				"description": "Permitir que o Roo crie e edite arquivos protegidos (como .rooignore e arquivos de configuração .roo/) sem exigir aprovação."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/ru/chat.json

@@ -140,6 +140,7 @@
 		"didRead": "Roo прочитал этот файл:",
 		"wantsToEdit": "Roo хочет отредактировать этот файл:",
 		"wantsToEditOutsideWorkspace": "Roo хочет отредактировать этот файл вне рабочей области:",
+		"wantsToEditProtected": "Roo хочет отредактировать защищённый файл конфигурации:",
 		"wantsToCreate": "Roo хочет создать новый файл:",
 		"wantsToSearchReplace": "Roo хочет выполнить поиск и замену в этом файле:",
 		"didSearchReplace": "Roo выполнил поиск и замену в этом файле:",

+ 4 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Включая файлы вне рабочей области",
 				"description": "Разрешить Roo создавать и редактировать файлы вне текущей рабочей области без необходимости одобрения."
+			},
+			"protected": {
+				"label": "Включить защищенные файлы",
+				"description": "Разрешить Roo создавать и редактировать защищенные файлы (такие как .rooignore и файлы конфигурации .roo/) без необходимости одобрения."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/tr/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo bu dosyayı okudu:",
 		"wantsToEdit": "Roo bu dosyayı düzenlemek istiyor:",
 		"wantsToEditOutsideWorkspace": "Roo çalışma alanı dışındaki bu dosyayı düzenlemek istiyor:",
+		"wantsToEditProtected": "Roo korumalı bir yapılandırma dosyasını düzenlemek istiyor:",
 		"wantsToCreate": "Roo yeni bir dosya oluşturmak istiyor:",
 		"wantsToSearchReplace": "Roo bu dosyada arama ve değiştirme yapmak istiyor:",
 		"didSearchReplace": "Roo bu dosyada arama ve değiştirme yaptı:",

+ 4 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Çalışma alanı dışındaki dosyaları dahil et",
 				"description": "Roo'nun onay gerektirmeden mevcut çalışma alanı dışında dosya oluşturmasına ve düzenlemesine izin ver."
+			},
+			"protected": {
+				"label": "Korumalı dosyaları dahil et",
+				"description": "Roo'nun korumalı dosyaları (.rooignore ve .roo/ yapılandırma dosyaları gibi) onay gerektirmeden oluşturmasına ve düzenlemesine izin ver."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/vi/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo đã đọc tệp này:",
 		"wantsToEdit": "Roo muốn chỉnh sửa tệp này:",
 		"wantsToEditOutsideWorkspace": "Roo muốn chỉnh sửa tệp này bên ngoài không gian làm việc:",
+		"wantsToEditProtected": "Roo muốn chỉnh sửa tệp cấu hình được bảo vệ:",
 		"wantsToCreate": "Roo muốn tạo một tệp mới:",
 		"wantsToSearchReplace": "Roo muốn thực hiện tìm kiếm và thay thế trong tệp này:",
 		"didSearchReplace": "Roo đã thực hiện tìm kiếm và thay thế trong tệp này:",

+ 4 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "Bao gồm các tệp ngoài không gian làm việc",
 				"description": "Cho phép Roo tạo và chỉnh sửa các tệp bên ngoài không gian làm việc hiện tại mà không yêu cầu phê duyệt."
+			},
+			"protected": {
+				"label": "Bao gồm các tệp được bảo vệ",
+				"description": "Cho phép Roo tạo và chỉnh sửa các tệp được bảo vệ (như .rooignore và các tệp cấu hình .roo/) mà không yêu cầu phê duyệt."
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "已读取文件:",
 		"wantsToEdit": "需要编辑文件:",
 		"wantsToEditOutsideWorkspace": "需要编辑外部文件:",
+		"wantsToEditProtected": "需要编辑受保护的配置文件:",
 		"wantsToCreate": "需要新建文件:",
 		"wantsToSearchReplace": "需要在此文件中搜索和替换:",
 		"didSearchReplace": "已完成搜索和替换:",

+ 4 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "包含工作区外的文件",
 				"description": "允许 Roo 创建和编辑当前工作区外的文件,无需批准。"
+			},
+			"protected": {
+				"label": "包含受保护的文件",
+				"description": "允许 Roo 创建和编辑受保护的文件(如 .rooignore 和 .roo/ 配置文件),无需批准。"
 			}
 		},
 		"browser": {

+ 1 - 0
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -145,6 +145,7 @@
 		"didRead": "Roo 已讀取此檔案:",
 		"wantsToEdit": "Roo 想要編輯此檔案:",
 		"wantsToEditOutsideWorkspace": "Roo 想要編輯此工作區外的檔案:",
+		"wantsToEditProtected": "Roo 想要編輯受保護的設定檔案:",
 		"wantsToCreate": "Roo 想要建立新檔案:",
 		"wantsToSearchReplace": "Roo 想要在此檔案中搜尋和取代:",
 		"didSearchReplace": "Roo 已在此檔案執行搜尋和取代:",

+ 4 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -83,6 +83,10 @@
 			"outsideWorkspace": {
 				"label": "包含工作區外的檔案",
 				"description": "允許 Roo 在目前工作區外建立和編輯檔案,無需核准。"
+			},
+			"protected": {
+				"label": "包含受保護的檔案",
+				"description": "允許 Roo 建立和編輯受保護的檔案(如 .rooignore 和 .roo/ 設定檔),無需核准。"
 			}
 		},
 		"browser": {