From ef8d49c7686bc4af398dc83eea774356810056b0 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 31 Jan 2025 15:15:31 -0800 Subject: [PATCH 01/24] initialize class --- src/core/Cline.ts | 9 +++++++++ .../llm-access-control/LLMFileAccessController.ts | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index ff462c61d0..96879e30f6 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -46,6 +46,7 @@ import { HistoryItem } from "../shared/HistoryItem" import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage" import { calculateApiCost } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" +import { LLMFileAccessController } from "../services/llm-access-control/LLMFileAccessController" import { arePathsEqual, getReadablePath } from "../utils/path" import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" @@ -80,6 +81,7 @@ export class Cline { private chatSettings: ChatSettings apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] + private llmAccessController: LLMFileAccessController private askResponse?: ClineAskResponse private askResponseText?: string private askResponseImages?: string[] @@ -123,6 +125,7 @@ export class Cline { images?: string[], historyItem?: HistoryItem, ) { + this.llmAccessController = new LLMFileAccessController(cwd) this.providerRef = new WeakRef(provider) this.api = buildApiHandler(apiConfiguration) this.terminalManager = new TerminalManager() @@ -748,6 +751,9 @@ export class Cline { // if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session) this.clineMessages = [] this.apiConversationHistory = [] + // Initialize the LLM access controller + await this.llmAccessController.initialize() + await this.providerRef.deref()?.postStateToWebview() await this.say("text", task, images) @@ -774,6 +780,9 @@ export class Cline { // this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks" // } + // Initialize the LLM access controller + await this.llmAccessController.initialize() + const modifiedClineMessages = await this.getSavedClineMessages() // Remove any resume messages that may have been added before diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index b5139c43a8..5410e8c823 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -40,6 +40,10 @@ export class LLMFileAccessController { try { const ignorePath = path.join(this.cwd, ".clineignore") if (await fileExistsAtPath(ignorePath)) { + // We need to reset ignore. Otherwise we will be adding duplicate patterns from re-reading the .clineignore + // Will be switching to globby in next PR + this.ignoreInstance = ignore() + this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) const content = await fs.readFile(ignorePath, "utf8") const customPatterns = content .split("\n") From 8e4ab703c8ccba9b8e979a0c1fc0cbd41c3a0350 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 31 Jan 2025 16:57:24 -0800 Subject: [PATCH 02/24] wip --- .../llm-access-control/LLMFileAccessController.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index 5410e8c823..298bfc08eb 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -2,6 +2,7 @@ import path from "path" import { fileExistsAtPath } from "../../utils/fs" import fs from "fs/promises" import ignore, { Ignore } from "ignore" +import * as vscode from "vscode" /** * Controls LLM access to files by enforcing ignore patterns. @@ -11,6 +12,8 @@ import ignore, { Ignore } from "ignore" export class LLMFileAccessController { private cwd: string private ignoreInstance: Ignore + private fileWatcher: vscode.FileSystemWatcher | undefined + private disposables: vscode.Disposable[] = [] /** * Default patterns that are always ignored for security @@ -20,9 +23,13 @@ export class LLMFileAccessController { constructor(cwd: string) { this.cwd = cwd this.ignoreInstance = ignore() - - // Add default patterns immediately this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + + // Set up file watcher for .clineignore + this.setupFileWatcher() + + // Load initial patterns + this.loadCustomPatterns() } /** From f6e64ccb9357554d1983251fdeab470f4856f910 Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 2 Feb 2025 14:19:12 -0800 Subject: [PATCH 03/24] wip --- src/core/Cline.ts | 14 ++- .../LLMFileAccessController.test.ts | 99 ++++++++++++++----- .../LLMFileAccessController.ts | 61 ++++++++++-- 3 files changed, 133 insertions(+), 41 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 96879e30f6..5dd9e3fe98 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -60,6 +60,7 @@ import { SYSTEM_PROMPT } from "./prompts/system" import { addUserInstructions } from "./prompts/system" import { OpenAiHandler } from "../api/providers/openai" import { ApiStream } from "../api/transform/stream" +import { Logger } from "../services/logging/Logger" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution @@ -125,7 +126,14 @@ export class Cline { images?: string[], historyItem?: HistoryItem, ) { + Logger.log("initializing LLMAC") this.llmAccessController = new LLMFileAccessController(cwd) + this.llmAccessController + .initialize() + .then(() => Logger.log("initialized")) + .catch((error) => { + console.error("Failed to initialize LLMFileAccessController:", error) + }) this.providerRef = new WeakRef(provider) this.api = buildApiHandler(apiConfiguration) this.terminalManager = new TerminalManager() @@ -751,8 +759,6 @@ export class Cline { // if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session) this.clineMessages = [] this.apiConversationHistory = [] - // Initialize the LLM access controller - await this.llmAccessController.initialize() await this.providerRef.deref()?.postStateToWebview() @@ -780,9 +786,6 @@ export class Cline { // this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks" // } - // Initialize the LLM access controller - await this.llmAccessController.initialize() - const modifiedClineMessages = await this.getSavedClineMessages() // Remove any resume messages that may have been added before @@ -1059,6 +1062,7 @@ export class Cline { this.terminalManager.disposeAll() this.urlContentFetcher.closeBrowser() this.browserSession.closeBrowser() + this.llmAccessController.dispose() await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint } diff --git a/src/services/llm-access-control/LLMFileAccessController.test.ts b/src/services/llm-access-control/LLMFileAccessController.test.ts index b8cee93e9a..c2620a9b5e 100644 --- a/src/services/llm-access-control/LLMFileAccessController.test.ts +++ b/src/services/llm-access-control/LLMFileAccessController.test.ts @@ -33,44 +33,44 @@ describe("LLMFileAccessController", () => { describe("Default Patterns", () => { // it("should block access to common ignored files", async () => { - // const results = await Promise.all([ + // const results = [ // controller.validateAccess(".env"), // controller.validateAccess(".git/config"), // controller.validateAccess("node_modules/package.json"), - // ]) + // ] // results.forEach((result) => result.should.be.false()) // }) it("should allow access to regular files", async () => { - const results = await Promise.all([ + const results = [ controller.validateAccess("src/index.ts"), controller.validateAccess("README.md"), controller.validateAccess("package.json"), - ]) + ] results.forEach((result) => result.should.be.true()) }) }) describe("Custom Patterns", () => { it("should block access to custom ignored patterns", async () => { - const results = await Promise.all([ + const results = [ controller.validateAccess("config.secret"), controller.validateAccess("private/data.txt"), controller.validateAccess("temp.json"), controller.validateAccess("nested/deep/file.secret"), controller.validateAccess("private/nested/deep/file.txt"), - ]) + ] results.forEach((result) => result.should.be.false()) }) it("should allow access to non-ignored files", async () => { - const results = await Promise.all([ + const results = [ controller.validateAccess("public/data.txt"), controller.validateAccess("config.json"), controller.validateAccess("src/temp/file.ts"), controller.validateAccess("nested/deep/file.txt"), controller.validateAccess("not-private/data.txt"), - ]) + ] results.forEach((result) => result.should.be.true()) }) @@ -83,11 +83,11 @@ describe("LLMFileAccessController", () => { controller = new LLMFileAccessController(tempDir) await controller.initialize() - const results = await Promise.all([ + const results = [ controller.validateAccess("data-123.json"), // Should be false (wildcard) controller.validateAccess("data.json"), // Should be true (doesn't match pattern) controller.validateAccess("script.tmp"), // Should be false (extension match) - ]) + ] results[0].should.be.false() // data-123.json results[1].should.be.true() // data.json @@ -112,9 +112,8 @@ describe("LLMFileAccessController", () => { // ) // controller = new LLMFileAccessController(tempDir) - // await controller.initialize() - // const results = await Promise.all([ + // const results = [ // // Basic negation // controller.validateAccess("temp/file.txt"), // Should be false (in temp/) // controller.validateAccess("temp/allowed/file.txt"), // Should be true (negated) @@ -130,7 +129,7 @@ describe("LLMFileAccessController", () => { // controller.validateAccess("assets/logo.png"), // Should be false (in assets/) // controller.validateAccess("assets/public/logo.png"), // Should be true (negated and matches *.png) // controller.validateAccess("assets/public/data.json"), // Should be true (in negated public/) - // ]) + // ] // results[0].should.be.false() // temp/file.txt // results[1].should.be.true() // temp/allowed/file.txt @@ -154,7 +153,7 @@ describe("LLMFileAccessController", () => { controller = new LLMFileAccessController(tempDir) await controller.initialize() - const result = await controller.validateAccess("test.secret") + const result = controller.validateAccess("test.secret") result.should.be.false() }) }) @@ -163,55 +162,55 @@ describe("LLMFileAccessController", () => { it("should handle absolute paths and match ignore patterns", async () => { // Test absolute path that should be allowed const allowedPath = path.join(tempDir, "src/file.ts") - const allowedResult = await controller.validateAccess(allowedPath) + const allowedResult = controller.validateAccess(allowedPath) allowedResult.should.be.true() // Test absolute path that matches an ignore pattern (*.secret) const ignoredPath = path.join(tempDir, "config.secret") - const ignoredResult = await controller.validateAccess(ignoredPath) + const ignoredResult = controller.validateAccess(ignoredPath) ignoredResult.should.be.false() // Test absolute path in ignored directory (private/) const ignoredDirPath = path.join(tempDir, "private/data.txt") - const ignoredDirResult = await controller.validateAccess(ignoredDirPath) + const ignoredDirResult = controller.validateAccess(ignoredDirPath) ignoredDirResult.should.be.false() }) it("should handle relative paths and match ignore patterns", async () => { // Test relative path that should be allowed - const allowedResult = await controller.validateAccess("./src/file.ts") + const allowedResult = controller.validateAccess("./src/file.ts") allowedResult.should.be.true() // Test relative path that matches an ignore pattern (*.secret) - const ignoredResult = await controller.validateAccess("./config.secret") + const ignoredResult = controller.validateAccess("./config.secret") ignoredResult.should.be.false() // Test relative path in ignored directory (private/) - const ignoredDirResult = await controller.validateAccess("./private/data.txt") + const ignoredDirResult = controller.validateAccess("./private/data.txt") ignoredDirResult.should.be.false() }) it("should normalize paths with backslashes", async () => { - const result = await controller.validateAccess("src\\file.ts") + const result = controller.validateAccess("src\\file.ts") result.should.be.true() }) it("should handle paths outside cwd", async () => { // Create a path that points to parent directory of cwd const outsidePath = path.join(path.dirname(tempDir), "outside.txt") - const result = await controller.validateAccess(outsidePath) + const result = controller.validateAccess(outsidePath) // Should return false for security since path is outside cwd result.should.be.false() // Test with a deeply nested path outside cwd const deepOutsidePath = path.join(path.dirname(tempDir), "deep", "nested", "outside.secret") - const deepResult = await controller.validateAccess(deepOutsidePath) + const deepResult = controller.validateAccess(deepOutsidePath) deepResult.should.be.false() // Test with a path that tries to escape using ../ const escapeAttemptPath = path.join(tempDir, "..", "escape-attempt.txt") - const escapeResult = await controller.validateAccess(escapeAttemptPath) + const escapeResult = controller.validateAccess(escapeAttemptPath) escapeResult.should.be.false() }) }) @@ -225,10 +224,56 @@ describe("LLMFileAccessController", () => { }) }) + describe("File Watcher", () => { + it("should update patterns when .clineignore is modified", async () => { + // Initial state + controller.validateAccess("test.log").should.be.true() + + // Modify .clineignore + await fs.writeFile(path.join(tempDir, ".clineignore"), "*.log") + + // Wait for the next event loop tick to ensure file watcher processes the change + await Promise.resolve() + + // Check if the new pattern is applied + controller.validateAccess("test.log").should.be.false() + }) + + it("should reset to default patterns when .clineignore is deleted", async () => { + // Initial state with .clineignore containing a pattern + await fs.writeFile(path.join(tempDir, ".clineignore"), "*.log") + await new Promise((resolve) => setTimeout(resolve, 100)) + controller.validateAccess("test.log").should.be.false() + + // Delete .clineignore + await fs.unlink(path.join(tempDir, ".clineignore")) + + // Wait for the next event loop tick to ensure file watcher processes the deletion + await Promise.resolve() + + // Should now allow previously ignored files + controller.validateAccess("test.log").should.be.true() + }) + + it("should handle .clineignore being recreated", async () => { + // Delete existing .clineignore + await fs.unlink(path.join(tempDir, ".clineignore")) + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Create new .clineignore with different patterns + await fs.writeFile(path.join(tempDir, ".clineignore"), "*.temp") + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Check if new patterns are applied + controller.validateAccess("file.temp").should.be.false() + controller.validateAccess("test.log").should.be.true() + }) + }) + describe("Error Handling", () => { it("should handle invalid paths", async () => { // Test with an invalid path containing null byte - const result = await controller.validateAccess("\0invalid") + const result = controller.validateAccess("\0invalid") result.should.be.true() }) @@ -240,7 +285,7 @@ describe("LLMFileAccessController", () => { try { const controller = new LLMFileAccessController(emptyDir) await controller.initialize() - const result = await controller.validateAccess("file.txt") + const result = controller.validateAccess("file.txt") result.should.be.true() } finally { await fs.rm(emptyDir, { recursive: true, force: true }) @@ -253,7 +298,7 @@ describe("LLMFileAccessController", () => { controller = new LLMFileAccessController(tempDir) await controller.initialize() - const result = await controller.validateAccess("regular-file.txt") + const result = controller.validateAccess("regular-file.txt") result.should.be.true() }) }) diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index 298bfc08eb..ad5ae50900 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -3,6 +3,7 @@ import { fileExistsAtPath } from "../../utils/fs" import fs from "fs/promises" import ignore, { Ignore } from "ignore" import * as vscode from "vscode" +import { Logger } from "../logging/Logger" /** * Controls LLM access to files by enforcing ignore patterns. @@ -27,44 +28,77 @@ export class LLMFileAccessController { // Set up file watcher for .clineignore this.setupFileWatcher() - - // Load initial patterns - this.loadCustomPatterns() } /** * Initialize the controller by loading custom patterns - * This must be called and awaited before using the controller + * Must be called after construction */ async initialize(): Promise { await this.loadCustomPatterns() } + /** + * Set up the file watcher for .clineignore changes + */ + private setupFileWatcher(): void { + const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore") + this.fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern) + + // Watch for changes and updates + this.disposables.push( + this.fileWatcher.onDidChange(() => { + Logger.log("[LLMFileAccessController] .clineignore changed - loading custom patterns") + this.loadCustomPatterns() + }), + this.fileWatcher.onDidCreate(() => { + Logger.log("[LLMFileAccessController] .clineignore created - loading custom patterns") + this.loadCustomPatterns() + }), + this.fileWatcher.onDidDelete(() => { + Logger.log("[LLMFileAccessController] .clineignore deleted - resetting to default patterns") + this.resetToDefaultPatterns() + }), + ) + + // Add fileWatcher itself to disposables + this.disposables.push(this.fileWatcher) + } + /** * Load custom patterns from .clineignore if it exists */ private async loadCustomPatterns(): Promise { try { + Logger.log("loading custom patterns.") const ignorePath = path.join(this.cwd, ".clineignore") if (await fileExistsAtPath(ignorePath)) { - // We need to reset ignore. Otherwise we will be adding duplicate patterns from re-reading the .clineignore - // Will be switching to globby in next PR - this.ignoreInstance = ignore() - this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + // Reset ignore instance to prevent duplicate patterns + this.resetToDefaultPatterns() const content = await fs.readFile(ignorePath, "utf8") const customPatterns = content .split("\n") .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#")) + Logger.log(`[LLMFileAccessController] Loading custom patterns: ${JSON.stringify(customPatterns)}`) this.ignoreInstance.add(customPatterns) } } catch (error) { - console.error("Failed to load .clineignore:", error) + Logger.log(`[LLMFileAccessController] Error loading .clineignore: ${error}`) // Continue with default patterns } } + /** + * Reset ignore patterns to defaults + */ + private resetToDefaultPatterns(): void { + Logger.log("[LLMFileAccessController] Resetting to default patterns") + this.ignoreInstance = ignore() + this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + } + /** * Check if a file should be accessible to the LLM * @param filePath - Path to check (relative to cwd) @@ -108,4 +142,13 @@ export class LLMFileAccessController { return [] // Fail closed for security } } + + /** + * Clean up resources when the controller is no longer needed + */ + dispose(): void { + this.disposables.forEach((d) => d.dispose()) + this.disposables = [] + this.fileWatcher = undefined + } } From fb613ef5c85afd66f418fc13c02afaeb4b045bc3 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 3 Feb 2025 08:47:57 -0800 Subject: [PATCH 04/24] cleaning up --- src/core/Cline.ts | 11 ++--- .../LLMFileAccessController.test.ts | 46 ------------------- .../LLMFileAccessController.ts | 13 ++---- 3 files changed, 6 insertions(+), 64 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 5dd9e3fe98..9af638085f 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -60,7 +60,6 @@ import { SYSTEM_PROMPT } from "./prompts/system" import { addUserInstructions } from "./prompts/system" import { OpenAiHandler } from "../api/providers/openai" import { ApiStream } from "../api/transform/stream" -import { Logger } from "../services/logging/Logger" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution @@ -126,14 +125,10 @@ export class Cline { images?: string[], historyItem?: HistoryItem, ) { - Logger.log("initializing LLMAC") this.llmAccessController = new LLMFileAccessController(cwd) - this.llmAccessController - .initialize() - .then(() => Logger.log("initialized")) - .catch((error) => { - console.error("Failed to initialize LLMFileAccessController:", error) - }) + this.llmAccessController.initialize().catch((error) => { + console.error("Failed to initialize LLMFileAccessController:", error) + }) this.providerRef = new WeakRef(provider) this.api = buildApiHandler(apiConfiguration) this.terminalManager = new TerminalManager() diff --git a/src/services/llm-access-control/LLMFileAccessController.test.ts b/src/services/llm-access-control/LLMFileAccessController.test.ts index c2620a9b5e..8bf6353a48 100644 --- a/src/services/llm-access-control/LLMFileAccessController.test.ts +++ b/src/services/llm-access-control/LLMFileAccessController.test.ts @@ -224,52 +224,6 @@ describe("LLMFileAccessController", () => { }) }) - describe("File Watcher", () => { - it("should update patterns when .clineignore is modified", async () => { - // Initial state - controller.validateAccess("test.log").should.be.true() - - // Modify .clineignore - await fs.writeFile(path.join(tempDir, ".clineignore"), "*.log") - - // Wait for the next event loop tick to ensure file watcher processes the change - await Promise.resolve() - - // Check if the new pattern is applied - controller.validateAccess("test.log").should.be.false() - }) - - it("should reset to default patterns when .clineignore is deleted", async () => { - // Initial state with .clineignore containing a pattern - await fs.writeFile(path.join(tempDir, ".clineignore"), "*.log") - await new Promise((resolve) => setTimeout(resolve, 100)) - controller.validateAccess("test.log").should.be.false() - - // Delete .clineignore - await fs.unlink(path.join(tempDir, ".clineignore")) - - // Wait for the next event loop tick to ensure file watcher processes the deletion - await Promise.resolve() - - // Should now allow previously ignored files - controller.validateAccess("test.log").should.be.true() - }) - - it("should handle .clineignore being recreated", async () => { - // Delete existing .clineignore - await fs.unlink(path.join(tempDir, ".clineignore")) - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Create new .clineignore with different patterns - await fs.writeFile(path.join(tempDir, ".clineignore"), "*.temp") - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Check if new patterns are applied - controller.validateAccess("file.temp").should.be.false() - controller.validateAccess("test.log").should.be.true() - }) - }) - describe("Error Handling", () => { it("should handle invalid paths", async () => { // Test with an invalid path containing null byte diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index ad5ae50900..9598bee9c9 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -3,7 +3,6 @@ import { fileExistsAtPath } from "../../utils/fs" import fs from "fs/promises" import ignore, { Ignore } from "ignore" import * as vscode from "vscode" -import { Logger } from "../logging/Logger" /** * Controls LLM access to files by enforcing ignore patterns. @@ -13,7 +12,7 @@ import { Logger } from "../logging/Logger" export class LLMFileAccessController { private cwd: string private ignoreInstance: Ignore - private fileWatcher: vscode.FileSystemWatcher | undefined + private fileWatcher: vscode.FileSystemWatcher | null private disposables: vscode.Disposable[] = [] /** @@ -25,6 +24,7 @@ export class LLMFileAccessController { this.cwd = cwd this.ignoreInstance = ignore() this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + this.fileWatcher = null // Set up file watcher for .clineignore this.setupFileWatcher() @@ -48,15 +48,12 @@ export class LLMFileAccessController { // Watch for changes and updates this.disposables.push( this.fileWatcher.onDidChange(() => { - Logger.log("[LLMFileAccessController] .clineignore changed - loading custom patterns") this.loadCustomPatterns() }), this.fileWatcher.onDidCreate(() => { - Logger.log("[LLMFileAccessController] .clineignore created - loading custom patterns") this.loadCustomPatterns() }), this.fileWatcher.onDidDelete(() => { - Logger.log("[LLMFileAccessController] .clineignore deleted - resetting to default patterns") this.resetToDefaultPatterns() }), ) @@ -70,7 +67,6 @@ export class LLMFileAccessController { */ private async loadCustomPatterns(): Promise { try { - Logger.log("loading custom patterns.") const ignorePath = path.join(this.cwd, ".clineignore") if (await fileExistsAtPath(ignorePath)) { // Reset ignore instance to prevent duplicate patterns @@ -81,11 +77,9 @@ export class LLMFileAccessController { .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#")) - Logger.log(`[LLMFileAccessController] Loading custom patterns: ${JSON.stringify(customPatterns)}`) this.ignoreInstance.add(customPatterns) } } catch (error) { - Logger.log(`[LLMFileAccessController] Error loading .clineignore: ${error}`) // Continue with default patterns } } @@ -94,7 +88,6 @@ export class LLMFileAccessController { * Reset ignore patterns to defaults */ private resetToDefaultPatterns(): void { - Logger.log("[LLMFileAccessController] Resetting to default patterns") this.ignoreInstance = ignore() this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) } @@ -149,6 +142,6 @@ export class LLMFileAccessController { dispose(): void { this.disposables.forEach((d) => d.dispose()) this.disposables = [] - this.fileWatcher = undefined + this.fileWatcher = null } } From 05093529bba35ae90d25ce0f4b996cc1c77e6cb3 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 3 Feb 2025 12:53:42 -0800 Subject: [PATCH 05/24] update comment --- src/services/llm-access-control/LLMFileAccessController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index 9598bee9c9..6499998613 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -32,7 +32,7 @@ export class LLMFileAccessController { /** * Initialize the controller by loading custom patterns - * Must be called after construction + * Must be called after construction and before using the controller */ async initialize(): Promise { await this.loadCustomPatterns() From 989be56f4b1da0a37b7b1565bab35b39df5f3371 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 3 Feb 2025 13:56:04 -0800 Subject: [PATCH 06/24] fire n forget error handling --- .../llm-access-control/LLMFileAccessController.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index 6499998613..40a2e46b55 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -48,10 +48,14 @@ export class LLMFileAccessController { // Watch for changes and updates this.disposables.push( this.fileWatcher.onDidChange(() => { - this.loadCustomPatterns() + this.loadCustomPatterns().catch((error) => { + console.error("Failed to load updated .clineignore patterns:", error) + }) }), this.fileWatcher.onDidCreate(() => { - this.loadCustomPatterns() + this.loadCustomPatterns().catch((error) => { + console.error("Failed to load new .clineignore patterns:", error) + }) }), this.fileWatcher.onDidDelete(() => { this.resetToDefaultPatterns() From 2a0558de803db07e02edcbd04081941273925e55 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 6 Feb 2025 14:51:43 -0800 Subject: [PATCH 07/24] files added --- src/core/Cline.ts | 87 +++++++++++++++---- src/core/prompts/responses.ts | 34 +++++++- src/core/prompts/system.ts | 1 + src/integrations/misc/extract-text.ts | 3 + src/services/glob/list-files.ts | 9 +- .../LLMFileAccessController.ts | 2 +- src/services/ripgrep/index.ts | 33 ++++--- src/services/tree-sitter/index.ts | 27 ++++-- src/utils/path.ts | 9 ++ 9 files changed, 161 insertions(+), 44 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 9af638085f..3722ca63b3 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -60,6 +60,7 @@ import { SYSTEM_PROMPT } from "./prompts/system" import { addUserInstructions } from "./prompts/system" import { OpenAiHandler } from "../api/providers/openai" import { ApiStream } from "../api/transform/stream" +import { Logger } from "../services/logging/Logger" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution @@ -81,7 +82,7 @@ export class Cline { private chatSettings: ChatSettings apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] - private llmAccessController: LLMFileAccessController + private llmFileAccessController: LLMFileAccessController private askResponse?: ClineAskResponse private askResponseText?: string private askResponseImages?: string[] @@ -125,8 +126,8 @@ export class Cline { images?: string[], historyItem?: HistoryItem, ) { - this.llmAccessController = new LLMFileAccessController(cwd) - this.llmAccessController.initialize().catch((error) => { + this.llmFileAccessController = new LLMFileAccessController(cwd) + this.llmFileAccessController.initialize().catch((error) => { console.error("Failed to initialize LLMFileAccessController:", error) }) this.providerRef = new WeakRef(provider) @@ -1057,7 +1058,7 @@ export class Cline { this.terminalManager.disposeAll() this.urlContentFetcher.closeBrowser() this.browserSession.closeBrowser() - this.llmAccessController.dispose() + this.llmFileAccessController.dispose() await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint } @@ -1584,6 +1585,13 @@ export class Cline { // wait so we can determine if it's a new file or editing an existing file break } + + const accessAllowed = this.llmFileAccessController.validateAccess(relPath) + if (!accessAllowed) { + await handleError("writing file", new Error(`Access denied: ${relPath} (blocked by .clineignore)`)) + break + } + // Check if file exists using cached map or fs.access let fileExists: boolean if (this.diffViewProvider.editType !== undefined) { @@ -1701,6 +1709,7 @@ export class Cline { await this.saveCheckpoint() break } + this.consecutiveMistakeCount = 0 // if isEditingFile false, that means we have the full contents of the file already. @@ -1852,6 +1861,16 @@ export class Cline { await this.saveCheckpoint() break } + + const accessAllowed = this.llmFileAccessController.validateAccess(relPath) + if (!accessAllowed) { + await handleError( + "reading file", + new Error(`Access denied: ${relPath} (blocked by .clineignore)`), + ) + break + } + this.consecutiveMistakeCount = 0 const absolutePath = path.resolve(cwd, relPath) const completeMessage = JSON.stringify({ @@ -1915,9 +1934,17 @@ export class Cline { break } this.consecutiveMistakeCount = 0 + const absolutePath = path.resolve(cwd, relDirPath) + const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200) - const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit) + + const result = formatResponse.formatFilesList( + absolutePath, + files, + didHitLimit, + this.llmFileAccessController, + ) const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result, @@ -1974,9 +2001,15 @@ export class Cline { await this.saveCheckpoint() break } + this.consecutiveMistakeCount = 0 + const absolutePath = path.resolve(cwd, relDirPath) - const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath) + const result = await parseSourceCodeForDefinitionsTopLevel( + absolutePath, + this.llmFileAccessController, + ) + const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result, @@ -2044,8 +2077,18 @@ export class Cline { break } this.consecutiveMistakeCount = 0 + const absolutePath = path.resolve(cwd, relDirPath) - const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern) + const clineignorePath = path.join(cwd, ".clineignore") + const results = await regexSearchFiles( + cwd, + absolutePath, + regex, + filePattern, + this.llmFileAccessController, + ) + // Logger.log(results) + const completeMessage = JSON.stringify({ ...sharedMessageProps, content: results, @@ -3183,26 +3226,38 @@ export class Cline { // It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context details += "\n\n# VSCode Visible Files" - const visibleFiles = vscode.window.visibleTextEditors + const visibleFilePaths = vscode.window.visibleTextEditors ?.map((editor) => editor.document?.uri?.fsPath) .filter(Boolean) - .map((absolutePath) => path.relative(cwd, absolutePath).toPosix()) + .map((absolutePath) => path.relative(cwd, absolutePath)) + + // Filter paths through LLMFileAccessController + const allowedVisibleFiles = this.llmFileAccessController + .filterPaths(visibleFilePaths) + .map((p) => p.toPosix()) .join("\n") - if (visibleFiles) { - details += `\n${visibleFiles}` + + if (allowedVisibleFiles) { + details += `\n${allowedVisibleFiles}` } else { details += "\n(No visible files)" } details += "\n\n# VSCode Open Tabs" - const openTabs = vscode.window.tabGroups.all + const openTabPaths = vscode.window.tabGroups.all .flatMap((group) => group.tabs) .map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath) .filter(Boolean) - .map((absolutePath) => path.relative(cwd, absolutePath).toPosix()) + .map((absolutePath) => path.relative(cwd, absolutePath)) + + // Filter paths through LLMFileAccessController + const allowedOpenTabs = this.llmFileAccessController + .filterPaths(openTabPaths) + .map((p) => p.toPosix()) .join("\n") - if (openTabs) { - details += `\n${openTabs}` + + if (allowedOpenTabs) { + details += `\n${allowedOpenTabs}` } else { details += "\n(No open tabs)" } @@ -3316,7 +3371,7 @@ export class Cline { details += "(Desktop files not shown automatically. Use list_files to explore if needed.)" } else { const [files, didHitLimit] = await listFiles(cwd, true, 200) - const result = formatResponse.formatFilesList(cwd, files, didHitLimit) + const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.llmFileAccessController) details += result } } diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index e932f4042c..d96fc9c18a 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -1,6 +1,8 @@ import { Anthropic } from "@anthropic-ai/sdk" import * as path from "path" import * as diff from "diff" +import { LLMFileAccessController } from "../../services/llm-access-control/LLMFileAccessController" +import { Logger } from "../../services/logging/Logger" export const formatResponse = { toolDenied: () => `The user denied this operation.`, @@ -46,7 +48,12 @@ Otherwise, if you have not completed the task and do not need additional informa return formatImagesIntoBlocks(images) }, - formatFilesList: (absolutePath: string, files: string[], didHitLimit: boolean): string => { + formatFilesList: ( + absolutePath: string, + files: string[], + didHitLimit: boolean, + llmFileAccessController: LLMFileAccessController, + ): string => { const sorted = files .map((file) => { // convert absolute path to relative path @@ -77,14 +84,33 @@ Otherwise, if you have not completed the task and do not need additional informa // the shorter one comes first return aParts.length - bParts.length }) + + const accessControlledSortedFiles = llmFileAccessController + ? sorted.map((filePath) => { + // path is relative to absolute path, not cwd + // validateAccess expects either path relative to cwd or absolute path + // otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored. + const absoluteFilePath = path.resolve(absolutePath, filePath) + const isIgnored = !llmFileAccessController.validateAccess(absoluteFilePath) + if (isIgnored) { + return "\u{1F512} " + filePath + } + + return filePath + }) + : sorted + if (didHitLimit) { - return `${sorted.join( + return `${accessControlledSortedFiles.join( "\n", )}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)` - } else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) { + } else if ( + accessControlledSortedFiles.length === 0 || + (accessControlledSortedFiles.length === 1 && accessControlledSortedFiles[0] === "") + ) { return "No files found." } else { - return sorted.join("\n") + return accessControlledSortedFiles.join("\n") } }, diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 3c26f70d75..250f7804e5 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -970,6 +970,7 @@ export function addUserInstructions(settingsCustomInstructions?: string, clineRu ==== USER'S CUSTOM INSTRUCTIONS +The user may have a ".clineignore" file that blocks you from accessing certain directories and files. The paths blocked by this file will be indicated to you with a \u{1F512}, but you need not mention the \u{1F512}. For example, do NOT say things like 'I notice that {some_file}.ts is marked with \u{1F512} in the file list.' Instead, if you ever encounter an error about being blocked by the .clineignore, don't make a big deal about it, just move on without remarking on it. If you absolutely have to say something, just make a casual remark that you are 'blocked by the .clineignore'. Note that the .clineignore is also blocked. The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index 67a580af9b..168eb6f5a3 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -4,8 +4,11 @@ import pdf from "pdf-parse/lib/pdf-parse" import mammoth from "mammoth" import fs from "fs/promises" import { isBinaryFile } from "isbinaryfile" +import { LLMFileAccessController } from "../../services/llm-access-control/LLMFileAccessController" export async function extractTextFromFile(filePath: string): Promise { + // First check if we have permission to access this file + try { await fs.access(filePath) } catch (error) { diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 8578b914d7..83c13fc31f 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -1,7 +1,8 @@ import { globby, Options } from "globby" import os from "os" import * as path from "path" -import { arePathsEqual } from "../../utils/path" +import { arePathsEqual, pathExists } from "../../utils/path" +import { Logger } from "../logging/Logger" export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> { const absolutePath = path.resolve(dirPath) @@ -45,9 +46,11 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults onlyFiles: false, // true by default, false means it will list directories on their own too } + // * globs all files in one dir, ** globs files in nested directories - const files = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit) - return [files, files.length >= limit] + const filePaths = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit) + + return [filePaths, filePaths.length >= limit] } /* diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index 40a2e46b55..69228751bc 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -18,7 +18,7 @@ export class LLMFileAccessController { /** * Default patterns that are always ignored for security */ - private static readonly DEFAULT_PATTERNS = [] // empty for now + private static readonly DEFAULT_PATTERNS = [".clineignore"] // empty for now constructor(cwd: string) { this.cwd = cwd diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index b9ebe68c77..da3600c2e8 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -1,8 +1,10 @@ import * as vscode from "vscode" import * as childProcess from "child_process" import * as path from "path" -import * as fs from "fs" import * as readline from "readline" +import { pathExists } from "../../utils/path" +import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController" +import { Logger } from "../logging/Logger" /* This file provides functionality to perform regex searches on files using ripgrep. @@ -50,7 +52,7 @@ const isWindows = /^win/.test(process.platform) const binName = isWindows ? "rg.exe" : "rg" interface SearchResult { - file: string + filePath: string line: number column: number match: string @@ -74,14 +76,6 @@ async function getBinPath(vscodeAppRoot: string): Promise { ) } -async function pathExists(path: string): Promise { - return new Promise((resolve) => { - fs.access(path, (err) => { - resolve(err === null) - }) - }) -} - async function execRipgrep(bin: string, args: string[]): Promise { return new Promise((resolve, reject) => { const rgProcess = childProcess.spawn(bin, args) @@ -122,7 +116,13 @@ async function execRipgrep(bin: string, args: string[]): Promise { }) } -export async function regexSearchFiles(cwd: string, directoryPath: string, regex: string, filePattern?: string): Promise { +export async function regexSearchFiles( + cwd: string, + directoryPath: string, + regex: string, + filePattern?: string, + llmFileAccessController?: LLMFileAccessController, +): Promise { const vscodeAppRoot = vscode.env.appRoot const rgPath = await getBinPath(vscodeAppRoot) @@ -150,7 +150,7 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex results.push(currentResult as SearchResult) } currentResult = { - file: parsed.data.path.text, + filePath: parsed.data.path.text, line: parsed.data.line_number, column: parsed.data.submatches[0].start, match: parsed.data.lines.text, @@ -174,7 +174,12 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex results.push(currentResult as SearchResult) } - return formatResults(results, cwd) + // Filter results using LLMFileAccessController if provided + const filteredResults = llmFileAccessController + ? results.filter((result) => llmFileAccessController.validateAccess(result.filePath)) + : results + + return formatResults(filteredResults, cwd) } function formatResults(results: SearchResult[], cwd: string): string { @@ -189,7 +194,7 @@ function formatResults(results: SearchResult[], cwd: string): string { // Group results by file name results.slice(0, MAX_RESULTS).forEach((result) => { - const relativeFilePath = path.relative(cwd, result.file) + const relativeFilePath = path.relative(cwd, result.filePath) if (!groupedResults[relativeFilePath]) { groupedResults[relativeFilePath] = [] } diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 19d0234f01..291127a6bf 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -3,9 +3,13 @@ import * as path from "path" import { listFiles } from "../glob/list-files" import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser" import { fileExistsAtPath } from "../../utils/fs" +import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController" // TODO: implement caching behavior to avoid having to keep analyzing project for new tasks. -export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise { +export async function parseSourceCodeForDefinitionsTopLevel( + dirPath: string, + llmFileAccessController?: LLMFileAccessController, +): Promise { // check if the path exists const dirExists = await fileExistsAtPath(path.resolve(dirPath)) if (!dirExists) { @@ -24,10 +28,14 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr // Parse specific files we have language parsers for // const filesWithoutDefinitions: string[] = [] - for (const file of filesToParse) { - const definitions = await parseFile(file, languageParsers) + + // Filter filepaths for access if controller is provided + const allowedFilesToParse = llmFileAccessController ? llmFileAccessController.filterPaths(filesToParse) : filesToParse + + for (const filePath of allowedFilesToParse) { + const definitions = await parseFile(filePath, languageParsers, llmFileAccessController) if (definitions) { - result += `${path.relative(dirPath, file).toPosix()}\n${definitions}\n` + result += `${path.relative(dirPath, filePath).toPosix()}\n${definitions}\n` } // else { // filesWithoutDefinitions.push(file) @@ -98,7 +106,14 @@ This approach allows us to focus on the most relevant parts of the code (defined - https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/helper.js - https://tree-sitter.github.io/tree-sitter/code-navigation-systems */ -async function parseFile(filePath: string, languageParsers: LanguageParser): Promise { +async function parseFile( + filePath: string, + languageParsers: LanguageParser, + llmFileAccessController?: LLMFileAccessController, +): Promise { + if (llmFileAccessController && !llmFileAccessController.validateAccess(filePath)) { + return null + } const fileContent = await fs.readFile(filePath, "utf8") const ext = path.extname(filePath).toLowerCase().slice(1) @@ -159,5 +174,5 @@ async function parseFile(filePath: string, languageParsers: LanguageParser): Pro if (formattedOutput.length > 0) { return `|----\n${formattedOutput}|----\n` } - return undefined + return null } diff --git a/src/utils/path.ts b/src/utils/path.ts index b61eb38bed..b9507be740 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,5 +1,6 @@ import * as path from "path" import os from "os" +import * as fs from "fs" /* The Node.js 'path' module resolves and normalizes paths differently depending on the platform: @@ -99,3 +100,11 @@ export function getReadablePath(cwd: string, relPath?: string): string { } } } + +export async function pathExists(path: string): Promise { + return new Promise((resolve) => { + fs.access(path, (err) => { + resolve(err === null) + }) + }) +} From a31b39428292e9b32082b9e102f9b9eee3806e23 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 6 Feb 2025 18:09:20 -0800 Subject: [PATCH 08/24] changeset --- .changeset/loud-countries-draw.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/loud-countries-draw.md diff --git a/.changeset/loud-countries-draw.md b/.changeset/loud-countries-draw.md new file mode 100644 index 0000000000..e290688320 --- /dev/null +++ b/.changeset/loud-countries-draw.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Introducing .clineignore From 6cfdd4fb906a4afc3573d2ce933cf08f8d80a1d9 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 6 Feb 2025 18:28:42 -0800 Subject: [PATCH 09/24] duplicate path function --- src/services/glob/list-files.ts | 2 +- src/services/ripgrep/index.ts | 4 ++-- src/utils/path.ts | 8 -------- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 3e13409557..745ac60d4d 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -1,7 +1,7 @@ import { globby, Options } from "globby" import os from "os" import * as path from "path" -import { arePathsEqual, pathExists } from "../../utils/path" +import { arePathsEqual } from "../../utils/path" export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> { const absolutePath = path.resolve(dirPath) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index ea3652b947..d6c0c72fe4 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -2,8 +2,8 @@ import * as vscode from "vscode" import * as childProcess from "child_process" import * as path from "path" import * as readline from "readline" -import { pathExists } from "../../utils/path" import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController" +import { fileExistsAtPath } from "../../utils/fs" /* This file provides functionality to perform regex searches on files using ripgrep. @@ -64,7 +64,7 @@ const MAX_RESULTS = 300 async function getBinPath(vscodeAppRoot: string): Promise { const checkPath = async (pkgFolder: string) => { const fullPath = path.join(vscodeAppRoot, pkgFolder, binName) - return (await pathExists(fullPath)) ? fullPath : undefined + return (await fileExistsAtPath(fullPath)) ? fullPath : undefined } return ( diff --git a/src/utils/path.ts b/src/utils/path.ts index b9507be740..90cff1c719 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -100,11 +100,3 @@ export function getReadablePath(cwd: string, relPath?: string): string { } } } - -export async function pathExists(path: string): Promise { - return new Promise((resolve) => { - fs.access(path, (err) => { - resolve(err === null) - }) - }) -} From fc60f6e55e195961414a23e2e70414652160205f Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 6 Feb 2025 18:49:11 -0800 Subject: [PATCH 10/24] cleanup --- src/core/Cline.ts | 1 - src/integrations/misc/extract-text.ts | 3 --- src/services/llm-access-control/LLMFileAccessController.ts | 4 ++-- src/utils/path.ts | 1 - 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 159460737b..a7e999f6af 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -2085,7 +2085,6 @@ export class Cline { this.consecutiveMistakeCount = 0 const absolutePath = path.resolve(cwd, relDirPath) - const clineignorePath = path.join(cwd, ".clineignore") const results = await regexSearchFiles( cwd, absolutePath, diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index 168eb6f5a3..67a580af9b 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -4,11 +4,8 @@ import pdf from "pdf-parse/lib/pdf-parse" import mammoth from "mammoth" import fs from "fs/promises" import { isBinaryFile } from "isbinaryfile" -import { LLMFileAccessController } from "../../services/llm-access-control/LLMFileAccessController" export async function extractTextFromFile(filePath: string): Promise { - // First check if we have permission to access this file - try { await fs.access(filePath) } catch (error) { diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index 69228751bc..2fa2f882f5 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -16,9 +16,9 @@ export class LLMFileAccessController { private disposables: vscode.Disposable[] = [] /** - * Default patterns that are always ignored for security + * Default patterns that are always ignored */ - private static readonly DEFAULT_PATTERNS = [".clineignore"] // empty for now + private static readonly DEFAULT_PATTERNS = [".clineignore"] constructor(cwd: string) { this.cwd = cwd diff --git a/src/utils/path.ts b/src/utils/path.ts index 90cff1c719..b61eb38bed 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,6 +1,5 @@ import * as path from "path" import os from "os" -import * as fs from "fs" /* The Node.js 'path' module resolves and normalizes paths differently depending on the platform: From 88a07de5fb3ab563f9677117d54c627c669e4c7e Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Thu, 6 Feb 2025 23:56:02 -0800 Subject: [PATCH 11/24] Use special error type for when cline tries to access clineignore'd file --- src/core/Cline.ts | 9 +++-- src/core/prompts/responses.ts | 3 ++ src/shared/ExtensionMessage.ts | 1 + webview-ui/src/components/chat/ChatRow.tsx | 41 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index a7e999f6af..2d0c785c2e 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1594,7 +1594,8 @@ export class Cline { const accessAllowed = this.llmFileAccessController.validateAccess(relPath) if (!accessAllowed) { - await handleError("writing file", new Error(`Access denied: ${relPath} (blocked by .clineignore)`)) + await this.say("clineignore_error", relPath) + pushToolResult(formatResponse.clineIgnoreError(relPath)) break } @@ -1870,10 +1871,8 @@ export class Cline { const accessAllowed = this.llmFileAccessController.validateAccess(relPath) if (!accessAllowed) { - await handleError( - "reading file", - new Error(`Access denied: ${relPath} (blocked by .clineignore)`), - ) + await this.say("clineignore_error", relPath) + pushToolResult(formatResponse.clineIgnoreError(relPath)) break } diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 34341dba1d..7452106d5f 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -11,6 +11,9 @@ export const formatResponse = { toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`, + clineIgnoreError: (path: string) => + `Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`, + noToolsUsed: () => `[ERROR] You did not use a tool in your previous response! Please retry with a tool use. diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 5a93caf07b..7f03c8e230 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -121,6 +121,7 @@ export type ClineSay = | "use_mcp_server" | "diff_error" | "deleted_api_reqs" + | "clineignore_error" export interface ClineSayTool { tool: diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 59d40936f1..a4904eb89e 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -989,6 +989,47 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi ) + case "clineignore_error": + return ( + <> +
+
+ + + Access Denied + +
+
+ Cline tried to access {message.text} which is blocked by the{" "} + .clineignore + file settings. +
+
+ + ) case "completion_result": const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text From 7f292bc9955828584a68e042d53d0a196bb041b6 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:25:29 -0800 Subject: [PATCH 12/24] Rename LLMFileAccessController to ClineIgnoreController --- src/core/Cline.ts | 32 +++++++++---------- .../ignore/ClineIgnoreController.test.ts} | 16 +++++----- .../ignore/ClineIgnoreController.ts} | 6 ++-- src/core/prompts/responses.ts | 10 +++--- src/services/ripgrep/index.ts | 10 +++--- src/services/tree-sitter/index.ts | 12 +++---- 6 files changed, 43 insertions(+), 43 deletions(-) rename src/{services/llm-access-control/LLMFileAccessController.test.ts => core/ignore/ClineIgnoreController.test.ts} (95%) rename src/{services/llm-access-control/LLMFileAccessController.ts => core/ignore/ClineIgnoreController.ts} (96%) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 2d0c785c2e..5a520f1177 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -46,7 +46,7 @@ import { HistoryItem } from "../shared/HistoryItem" import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage" import { calculateApiCost } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" -import { LLMFileAccessController } from "../services/llm-access-control/LLMFileAccessController" +import { ClineIgnoreController } from "./ignore/ClineIgnoreController" import { arePathsEqual, getReadablePath } from "../utils/path" import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" @@ -81,7 +81,7 @@ export class Cline { private chatSettings: ChatSettings apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] - private llmFileAccessController: LLMFileAccessController + private clineIgnoreController: ClineIgnoreController private askResponse?: ClineAskResponse private askResponseText?: string private askResponseImages?: string[] @@ -125,9 +125,9 @@ export class Cline { images?: string[], historyItem?: HistoryItem, ) { - this.llmFileAccessController = new LLMFileAccessController(cwd) - this.llmFileAccessController.initialize().catch((error) => { - console.error("Failed to initialize LLMFileAccessController:", error) + this.clineIgnoreController = new ClineIgnoreController(cwd) + this.clineIgnoreController.initialize().catch((error) => { + console.error("Failed to initialize ClineIgnoreController:", error) }) this.providerRef = new WeakRef(provider) this.api = buildApiHandler(apiConfiguration) @@ -1057,7 +1057,7 @@ export class Cline { this.terminalManager.disposeAll() this.urlContentFetcher.closeBrowser() this.browserSession.closeBrowser() - this.llmFileAccessController.dispose() + this.clineIgnoreController.dispose() await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint } @@ -1592,7 +1592,7 @@ export class Cline { break } - const accessAllowed = this.llmFileAccessController.validateAccess(relPath) + const accessAllowed = this.clineIgnoreController.validateAccess(relPath) if (!accessAllowed) { await this.say("clineignore_error", relPath) pushToolResult(formatResponse.clineIgnoreError(relPath)) @@ -1869,7 +1869,7 @@ export class Cline { break } - const accessAllowed = this.llmFileAccessController.validateAccess(relPath) + const accessAllowed = this.clineIgnoreController.validateAccess(relPath) if (!accessAllowed) { await this.say("clineignore_error", relPath) pushToolResult(formatResponse.clineIgnoreError(relPath)) @@ -1948,7 +1948,7 @@ export class Cline { absolutePath, files, didHitLimit, - this.llmFileAccessController, + this.clineIgnoreController, ) const completeMessage = JSON.stringify({ ...sharedMessageProps, @@ -2012,7 +2012,7 @@ export class Cline { const absolutePath = path.resolve(cwd, relDirPath) const result = await parseSourceCodeForDefinitionsTopLevel( absolutePath, - this.llmFileAccessController, + this.clineIgnoreController, ) const completeMessage = JSON.stringify({ @@ -2089,7 +2089,7 @@ export class Cline { absolutePath, regex, filePattern, - this.llmFileAccessController, + this.clineIgnoreController, ) const completeMessage = JSON.stringify({ @@ -3236,8 +3236,8 @@ export class Cline { .filter(Boolean) .map((absolutePath) => path.relative(cwd, absolutePath)) - // Filter paths through LLMFileAccessController - const allowedVisibleFiles = this.llmFileAccessController + // Filter paths through clineIgnoreController + const allowedVisibleFiles = this.clineIgnoreController .filterPaths(visibleFilePaths) .map((p) => p.toPosix()) .join("\n") @@ -3255,8 +3255,8 @@ export class Cline { .filter(Boolean) .map((absolutePath) => path.relative(cwd, absolutePath)) - // Filter paths through LLMFileAccessController - const allowedOpenTabs = this.llmFileAccessController + // Filter paths through clineIgnoreController + const allowedOpenTabs = this.clineIgnoreController .filterPaths(openTabPaths) .map((p) => p.toPosix()) .join("\n") @@ -3376,7 +3376,7 @@ export class Cline { details += "(Desktop files not shown automatically. Use list_files to explore if needed.)" } else { const [files, didHitLimit] = await listFiles(cwd, true, 200) - const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.llmFileAccessController) + const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.clineIgnoreController) details += result } } diff --git a/src/services/llm-access-control/LLMFileAccessController.test.ts b/src/core/ignore/ClineIgnoreController.test.ts similarity index 95% rename from src/services/llm-access-control/LLMFileAccessController.test.ts rename to src/core/ignore/ClineIgnoreController.test.ts index 8bf6353a48..084ad311c1 100644 --- a/src/services/llm-access-control/LLMFileAccessController.test.ts +++ b/src/core/ignore/ClineIgnoreController.test.ts @@ -1,13 +1,13 @@ -import { LLMFileAccessController } from "./LLMFileAccessController" +import { ClineIgnoreController } from "./ClineIgnoreController" import fs from "fs/promises" import path from "path" import os from "os" import { after, beforeEach, describe, it } from "mocha" import "should" -describe("LLMFileAccessController", () => { +describe("ClineIgnoreController", () => { let tempDir: string - let controller: LLMFileAccessController + let controller: ClineIgnoreController beforeEach(async () => { // Create a temp directory for testing @@ -22,7 +22,7 @@ describe("LLMFileAccessController", () => { ), ) - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() }) @@ -80,7 +80,7 @@ describe("LLMFileAccessController", () => { ["*.secret", "private/", "*.tmp", "data-*.json", "temp/*"].join("\n"), ) - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() const results = [ @@ -150,7 +150,7 @@ describe("LLMFileAccessController", () => { ["# Comment line", "*.secret", "private/", "temp.*"].join("\n"), ) - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() const result = controller.validateAccess("test.secret") @@ -237,7 +237,7 @@ describe("LLMFileAccessController", () => { await fs.mkdir(emptyDir) try { - const controller = new LLMFileAccessController(emptyDir) + const controller = new ClineIgnoreController(emptyDir) await controller.initialize() const result = controller.validateAccess("file.txt") result.should.be.true() @@ -249,7 +249,7 @@ describe("LLMFileAccessController", () => { it("should handle empty .clineignore", async () => { await fs.writeFile(path.join(tempDir, ".clineignore"), "") - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() const result = controller.validateAccess("regular-file.txt") diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/core/ignore/ClineIgnoreController.ts similarity index 96% rename from src/services/llm-access-control/LLMFileAccessController.ts rename to src/core/ignore/ClineIgnoreController.ts index 2fa2f882f5..bcb02989e8 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -9,7 +9,7 @@ import * as vscode from "vscode" * Designed to be instantiated once in Cline.ts and passed to file manipulation services. * Uses the 'ignore' library to support standard .gitignore syntax in .clineignore files. */ -export class LLMFileAccessController { +export class ClineIgnoreController { private cwd: string private ignoreInstance: Ignore private fileWatcher: vscode.FileSystemWatcher | null @@ -23,7 +23,7 @@ export class LLMFileAccessController { constructor(cwd: string) { this.cwd = cwd this.ignoreInstance = ignore() - this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + this.ignoreInstance.add(ClineIgnoreController.DEFAULT_PATTERNS) this.fileWatcher = null // Set up file watcher for .clineignore @@ -93,7 +93,7 @@ export class LLMFileAccessController { */ private resetToDefaultPatterns(): void { this.ignoreInstance = ignore() - this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + this.ignoreInstance.add(ClineIgnoreController.DEFAULT_PATTERNS) } /** diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 7452106d5f..5cce47e9e4 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -1,7 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" -import * as path from "path" import * as diff from "diff" -import { LLMFileAccessController } from "../../services/llm-access-control/LLMFileAccessController" +import * as path from "path" +import { ClineIgnoreController } from "../ignore/ClineIgnoreController" export const formatResponse = { toolDenied: () => `The user denied this operation.`, @@ -54,7 +54,7 @@ Otherwise, if you have not completed the task and do not need additional informa absolutePath: string, files: string[], didHitLimit: boolean, - llmFileAccessController: LLMFileAccessController, + clineIgnoreController: ClineIgnoreController, ): string => { const sorted = files .map((file) => { @@ -87,13 +87,13 @@ Otherwise, if you have not completed the task and do not need additional informa return aParts.length - bParts.length }) - const accessControlledSortedFiles = llmFileAccessController + const accessControlledSortedFiles = clineIgnoreController ? sorted.map((filePath) => { // path is relative to absolute path, not cwd // validateAccess expects either path relative to cwd or absolute path // otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored. const absoluteFilePath = path.resolve(absolutePath, filePath) - const isIgnored = !llmFileAccessController.validateAccess(absoluteFilePath) + const isIgnored = !clineIgnoreController.validateAccess(absoluteFilePath) if (isIgnored) { return "\u{1F512} " + filePath } diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index d6c0c72fe4..21d3d9d0df 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -2,8 +2,8 @@ import * as vscode from "vscode" import * as childProcess from "child_process" import * as path from "path" import * as readline from "readline" -import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController" import { fileExistsAtPath } from "../../utils/fs" +import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController" /* This file provides functionality to perform regex searches on files using ripgrep. @@ -120,7 +120,7 @@ export async function regexSearchFiles( directoryPath: string, regex: string, filePattern?: string, - llmFileAccessController?: LLMFileAccessController, + clineIgnoreController?: ClineIgnoreController, ): Promise { const vscodeAppRoot = vscode.env.appRoot const rgPath = await getBinPath(vscodeAppRoot) @@ -173,9 +173,9 @@ export async function regexSearchFiles( results.push(currentResult as SearchResult) } - // Filter results using LLMFileAccessController if provided - const filteredResults = llmFileAccessController - ? results.filter((result) => llmFileAccessController.validateAccess(result.filePath)) + // Filter results using ClineIgnoreController if provided + const filteredResults = clineIgnoreController + ? results.filter((result) => clineIgnoreController.validateAccess(result.filePath)) : results return formatResults(filteredResults, cwd) diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 291127a6bf..262e3cd5cc 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -3,12 +3,12 @@ import * as path from "path" import { listFiles } from "../glob/list-files" import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser" import { fileExistsAtPath } from "../../utils/fs" -import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController" +import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController" // TODO: implement caching behavior to avoid having to keep analyzing project for new tasks. export async function parseSourceCodeForDefinitionsTopLevel( dirPath: string, - llmFileAccessController?: LLMFileAccessController, + clineIgnoreController?: ClineIgnoreController, ): Promise { // check if the path exists const dirExists = await fileExistsAtPath(path.resolve(dirPath)) @@ -30,10 +30,10 @@ export async function parseSourceCodeForDefinitionsTopLevel( // const filesWithoutDefinitions: string[] = [] // Filter filepaths for access if controller is provided - const allowedFilesToParse = llmFileAccessController ? llmFileAccessController.filterPaths(filesToParse) : filesToParse + const allowedFilesToParse = clineIgnoreController ? clineIgnoreController.filterPaths(filesToParse) : filesToParse for (const filePath of allowedFilesToParse) { - const definitions = await parseFile(filePath, languageParsers, llmFileAccessController) + const definitions = await parseFile(filePath, languageParsers, clineIgnoreController) if (definitions) { result += `${path.relative(dirPath, filePath).toPosix()}\n${definitions}\n` } @@ -109,9 +109,9 @@ This approach allows us to focus on the most relevant parts of the code (defined async function parseFile( filePath: string, languageParsers: LanguageParser, - llmFileAccessController?: LLMFileAccessController, + clineIgnoreController?: ClineIgnoreController, ): Promise { - if (llmFileAccessController && !llmFileAccessController.validateAccess(filePath)) { + if (clineIgnoreController && !clineIgnoreController.validateAccess(filePath)) { return null } const fileContent = await fs.readFile(filePath, "utf8") From f9e310f3383b4231afbe2fd7c8831a3f07f5cbbc Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:26:42 -0800 Subject: [PATCH 13/24] Use better changeset --- .changeset/loud-countries-draw.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/loud-countries-draw.md b/.changeset/loud-countries-draw.md index e290688320..3a6e55f337 100644 --- a/.changeset/loud-countries-draw.md +++ b/.changeset/loud-countries-draw.md @@ -2,4 +2,4 @@ "claude-dev": minor --- -Introducing .clineignore +Add .clineignore file to block Cline from accessing specified file patterns \ No newline at end of file From 4f2d5b15bf892fd2994838a16e23fb32c689a9c0 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:37:03 -0800 Subject: [PATCH 14/24] Remove unnecessary file parsing --- src/core/ignore/ClineIgnoreController.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts index bcb02989e8..68132145c8 100644 --- a/src/core/ignore/ClineIgnoreController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -76,12 +76,7 @@ export class ClineIgnoreController { // Reset ignore instance to prevent duplicate patterns this.resetToDefaultPatterns() const content = await fs.readFile(ignorePath, "utf8") - const customPatterns = content - .split("\n") - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")) - - this.ignoreInstance.add(customPatterns) + this.ignoreInstance.add(content) } } catch (error) { // Continue with default patterns From 88c75a06f2687d7648dbd9294c2745516fc0c791 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:58:20 -0800 Subject: [PATCH 15/24] Allow access to files outside cwd --- src/core/ignore/ClineIgnoreController.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts index 68132145c8..c1eb037d8d 100644 --- a/src/core/ignore/ClineIgnoreController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -100,18 +100,14 @@ export class ClineIgnoreController { 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).replace(/\\/g, "/") + const relativePath = path.relative(this.cwd, absolutePath).toPosix() - // Block access to paths outside cwd (those starting with '..') - if (relativePath.startsWith("..")) { - return false - } - - // Use ignore library to check if path should be ignored + // Ignore expects paths to be path.relative()'d return !this.ignoreInstance.ignores(relativePath) } catch (error) { - console.error(`Error validating access for ${filePath}:`, error) - return false // Fail closed for security + // console.error(`Error validating access for ${filePath}:`, error) + // Ignore is designed to work with relative file paths, so will throw error for paths outside cwd. We are allowing access to all files outside cwd. + return true } } From 8ae39e24be210bfbfe44fdecdca62c28476cfb43 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:33:47 -0800 Subject: [PATCH 16/24] Use more efficient clineIgnoreExists --- src/core/ignore/ClineIgnoreController.ts | 58 ++++++++++-------------- src/core/prompts/responses.ts | 13 ++---- 2 files changed, 28 insertions(+), 43 deletions(-) diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts index c1eb037d8d..0fb882e121 100644 --- a/src/core/ignore/ClineIgnoreController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -12,20 +12,13 @@ import * as vscode from "vscode" export class ClineIgnoreController { private cwd: string private ignoreInstance: Ignore - private fileWatcher: vscode.FileSystemWatcher | null private disposables: vscode.Disposable[] = [] - - /** - * Default patterns that are always ignored - */ - private static readonly DEFAULT_PATTERNS = [".clineignore"] + clineIgnoreExists: boolean constructor(cwd: string) { this.cwd = cwd this.ignoreInstance = ignore() - this.ignoreInstance.add(ClineIgnoreController.DEFAULT_PATTERNS) - this.fileWatcher = null - + this.clineIgnoreExists = false // Set up file watcher for .clineignore this.setupFileWatcher() } @@ -35,7 +28,7 @@ export class ClineIgnoreController { * Must be called after construction and before using the controller */ async initialize(): Promise { - await this.loadCustomPatterns() + await this.loadClineIgnore() } /** @@ -43,60 +36,56 @@ export class ClineIgnoreController { */ private setupFileWatcher(): void { const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore") - this.fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern) + const fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern) // Watch for changes and updates this.disposables.push( - this.fileWatcher.onDidChange(() => { - this.loadCustomPatterns().catch((error) => { - console.error("Failed to load updated .clineignore patterns:", error) - }) + fileWatcher.onDidChange(() => { + this.loadClineIgnore() }), - this.fileWatcher.onDidCreate(() => { - this.loadCustomPatterns().catch((error) => { - console.error("Failed to load new .clineignore patterns:", error) - }) + fileWatcher.onDidCreate(() => { + this.loadClineIgnore() }), - this.fileWatcher.onDidDelete(() => { - this.resetToDefaultPatterns() + fileWatcher.onDidDelete(() => { + this.loadClineIgnore() }), ) // Add fileWatcher itself to disposables - this.disposables.push(this.fileWatcher) + this.disposables.push(fileWatcher) } /** * Load custom patterns from .clineignore if it exists */ - private async loadCustomPatterns(): Promise { + private async loadClineIgnore(): Promise { try { + // Reset ignore instance to prevent duplicate patterns + this.ignoreInstance = ignore() const ignorePath = path.join(this.cwd, ".clineignore") if (await fileExistsAtPath(ignorePath)) { - // Reset ignore instance to prevent duplicate patterns - this.resetToDefaultPatterns() + this.clineIgnoreExists = true const content = await fs.readFile(ignorePath, "utf8") this.ignoreInstance.add(content) + } else { + this.clineIgnoreExists = false } } catch (error) { - // Continue with default patterns + // Should never happen: reading file failed even though it exists + console.error("Unexpected error loading .clineignore:", error) } } - /** - * Reset ignore patterns to defaults - */ - private resetToDefaultPatterns(): void { - this.ignoreInstance = ignore() - this.ignoreInstance.add(ClineIgnoreController.DEFAULT_PATTERNS) - } - /** * Check if a file should be accessible to the LLM * @param filePath - Path to check (relative to cwd) * @returns true if file is accessible, false if ignored */ validateAccess(filePath: string): boolean { + // Always allow access if .clineignore does not exist + if (!this.clineIgnoreExists) { + return true + } try { // Normalize path to be relative to cwd and use forward slashes const absolutePath = path.resolve(this.cwd, filePath) @@ -137,6 +126,5 @@ export class ClineIgnoreController { dispose(): void { this.disposables.forEach((d) => d.dispose()) this.disposables = [] - this.fileWatcher = null } } diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 5cce47e9e4..090667d3ff 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -54,7 +54,7 @@ Otherwise, if you have not completed the task and do not need additional informa absolutePath: string, files: string[], didHitLimit: boolean, - clineIgnoreController: ClineIgnoreController, + clineIgnoreController?: ClineIgnoreController, ): string => { const sorted = files .map((file) => { @@ -87,7 +87,7 @@ Otherwise, if you have not completed the task and do not need additional informa return aParts.length - bParts.length }) - const accessControlledSortedFiles = clineIgnoreController + const clineIgnoreParsed = clineIgnoreController ? sorted.map((filePath) => { // path is relative to absolute path, not cwd // validateAccess expects either path relative to cwd or absolute path @@ -103,16 +103,13 @@ Otherwise, if you have not completed the task and do not need additional informa : sorted if (didHitLimit) { - return `${accessControlledSortedFiles.join( + return `${clineIgnoreParsed.join( "\n", )}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)` - } else if ( - accessControlledSortedFiles.length === 0 || - (accessControlledSortedFiles.length === 1 && accessControlledSortedFiles[0] === "") - ) { + } else if (clineIgnoreParsed.length === 0 || (clineIgnoreParsed.length === 1 && clineIgnoreParsed[0] === "")) { return "No files found." } else { - return accessControlledSortedFiles.join("\n") + return clineIgnoreParsed.join("\n") } }, From 53604c94131e9125ab68ae30d2f905f0a1378b9e Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:51:22 -0800 Subject: [PATCH 17/24] Modify clineignore prompt --- src/core/Cline.ts | 8 +++++++- src/core/ignore/ClineIgnoreController.ts | 10 +++++----- src/core/prompts/system.ts | 12 +++++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 5a520f1177..81d97a087f 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1242,9 +1242,15 @@ export class Cline { } } + const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent + let clineIgnoreInstructions: string | undefined + if (clineIgnoreContent) { + clineIgnoreInstructions = `# .clineignore\n\nThe following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a \u{1F512} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.\n\n${clineIgnoreContent}` + } + if (settingsCustomInstructions || clineRulesFileInstructions) { // altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with - systemPrompt += addUserInstructions(settingsCustomInstructions, clineRulesFileInstructions) + systemPrompt += addUserInstructions(settingsCustomInstructions, clineRulesFileInstructions, clineIgnoreInstructions) } // If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts index 0fb882e121..1ed8097382 100644 --- a/src/core/ignore/ClineIgnoreController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -13,12 +13,12 @@ export class ClineIgnoreController { private cwd: string private ignoreInstance: Ignore private disposables: vscode.Disposable[] = [] - clineIgnoreExists: boolean + clineIgnoreContent: string | undefined constructor(cwd: string) { this.cwd = cwd this.ignoreInstance = ignore() - this.clineIgnoreExists = false + this.clineIgnoreContent = undefined // Set up file watcher for .clineignore this.setupFileWatcher() } @@ -64,11 +64,11 @@ export class ClineIgnoreController { this.ignoreInstance = ignore() const ignorePath = path.join(this.cwd, ".clineignore") if (await fileExistsAtPath(ignorePath)) { - this.clineIgnoreExists = true const content = await fs.readFile(ignorePath, "utf8") + this.clineIgnoreContent = content this.ignoreInstance.add(content) } else { - this.clineIgnoreExists = false + this.clineIgnoreContent = undefined } } catch (error) { // Should never happen: reading file failed even though it exists @@ -83,7 +83,7 @@ export class ClineIgnoreController { */ validateAccess(filePath: string): boolean { // Always allow access if .clineignore does not exist - if (!this.clineIgnoreExists) { + if (!this.clineIgnoreContent) { return true } try { diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index d84391bed2..a9a13f1e90 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -957,20 +957,26 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. 5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.` -export function addUserInstructions(settingsCustomInstructions?: string, clineRulesFileInstructions?: string) { +export function addUserInstructions( + settingsCustomInstructions?: string, + clineRulesFileInstructions?: string, + clineIgnoreInstructions?: string, +) { let customInstructions = "" if (settingsCustomInstructions) { customInstructions += settingsCustomInstructions + "\n\n" } if (clineRulesFileInstructions) { - customInstructions += clineRulesFileInstructions + customInstructions += clineRulesFileInstructions + "\n\n" + } + if (clineIgnoreInstructions) { + customInstructions += clineIgnoreInstructions } return ` ==== USER'S CUSTOM INSTRUCTIONS -The user may have a ".clineignore" file that blocks you from accessing certain directories and files. The paths blocked by this file will be indicated to you with a \u{1F512}, but you need not mention the \u{1F512}. For example, do NOT say things like 'I notice that {some_file}.ts is marked with \u{1F512} in the file list.' Instead, if you ever encounter an error about being blocked by the .clineignore, don't make a big deal about it, just move on without remarking on it. If you absolutely have to say something, just make a casual remark that you are 'blocked by the .clineignore'. Note that the .clineignore is also blocked. The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. From 075e3171a8f3b96b63fb14a4c3ea5d9217f62a28 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:52:57 -0800 Subject: [PATCH 18/24] Use LOCK_TEXT_SYMBOL --- src/core/Cline.ts | 15 +++++++-------- src/core/ignore/ClineIgnoreController.ts | 2 ++ src/core/prompts/responses.ts | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 81d97a087f..5b83218c14 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -9,6 +9,9 @@ import * as path from "path" import { serializeError } from "serialize-error" import * as vscode from "vscode" import { ApiHandler, buildApiHandler } from "../api" +import { OpenAiHandler } from "../api/providers/openai" +import { OpenRouterHandler } from "../api/providers/openrouter" +import { ApiStream } from "../api/transform/stream" import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker" import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider" import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown" @@ -46,20 +49,16 @@ import { HistoryItem } from "../shared/HistoryItem" import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage" import { calculateApiCost } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" -import { ClineIgnoreController } from "./ignore/ClineIgnoreController" import { arePathsEqual, getReadablePath } from "../utils/path" import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" import { constructNewFileContent } from "./assistant-message/diff" +import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/ClineIgnoreController" import { parseMentions } from "./mentions" import { formatResponse } from "./prompts/responses" -import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider" -import { OpenRouterHandler } from "../api/providers/openrouter" +import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system" import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window" -import { SYSTEM_PROMPT } from "./prompts/system" -import { addUserInstructions } from "./prompts/system" -import { OpenAiHandler } from "../api/providers/openai" -import { ApiStream } from "../api/transform/stream" +import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution @@ -1245,7 +1244,7 @@ export class Cline { const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent let clineIgnoreInstructions: string | undefined if (clineIgnoreContent) { - clineIgnoreInstructions = `# .clineignore\n\nThe following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a \u{1F512} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.\n\n${clineIgnoreContent}` + clineIgnoreInstructions = `# .clineignore\n\nThe following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.\n\n${clineIgnoreContent}` } if (settingsCustomInstructions || clineRulesFileInstructions) { diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts index 1ed8097382..925dcd189a 100644 --- a/src/core/ignore/ClineIgnoreController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -4,6 +4,8 @@ import fs from "fs/promises" import ignore, { Ignore } from "ignore" import * as vscode from "vscode" +export const LOCK_TEXT_SYMBOL = "\u{1F512}" + /** * Controls LLM access to files by enforcing ignore patterns. * Designed to be instantiated once in Cline.ts and passed to file manipulation services. diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 090667d3ff..623e3d8806 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -1,7 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import * as diff from "diff" import * as path from "path" -import { ClineIgnoreController } from "../ignore/ClineIgnoreController" +import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController" export const formatResponse = { toolDenied: () => `The user denied this operation.`, @@ -95,7 +95,7 @@ Otherwise, if you have not completed the task and do not need additional informa const absoluteFilePath = path.resolve(absolutePath, filePath) const isIgnored = !clineIgnoreController.validateAccess(absoluteFilePath) if (isIgnored) { - return "\u{1F512} " + filePath + return LOCK_TEXT_SYMBOL + " " + filePath } return filePath From 5585c684e9f1f7555deaef2b423189425b33400a Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:46:44 -0800 Subject: [PATCH 19/24] Block commands attempting to access clineignored files --- src/core/Cline.ts | 10 +++++ src/core/ignore/ClineIgnoreController.ts | 56 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 5b83218c14..f6ae751837 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1601,6 +1601,7 @@ export class Cline { if (!accessAllowed) { await this.say("clineignore_error", relPath) pushToolResult(formatResponse.clineIgnoreError(relPath)) + await this.saveCheckpoint() break } @@ -1878,6 +1879,7 @@ export class Cline { if (!accessAllowed) { await this.say("clineignore_error", relPath) pushToolResult(formatResponse.clineIgnoreError(relPath)) + await this.saveCheckpoint() break } @@ -2333,6 +2335,14 @@ export class Cline { } this.consecutiveMistakeCount = 0 + const ignoredFileAttemptedToAccess = this.clineIgnoreController.validateCommand(command) + if (ignoredFileAttemptedToAccess) { + await this.say("clineignore_error", ignoredFileAttemptedToAccess) + pushToolResult(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)) + await this.saveCheckpoint() + break + } + let didAutoApprove = false if (!requiresApproval && this.shouldAutoApproveTool(block.name)) { diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts index 925dcd189a..d3637caa76 100644 --- a/src/core/ignore/ClineIgnoreController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -102,6 +102,62 @@ export class ClineIgnoreController { } } + /** + * Check if a terminal command should be allowed to execute based on file access patterns + * @param command - Terminal command to validate + * @returns path of file that is being accessed if it is being accessed, undefined if command is allowed + */ + validateCommand(command: string): string | undefined { + // Always allow if no .clineignore exists + if (!this.clineIgnoreContent) { + return undefined + } + + // Split command into parts and get the base command + const parts = command.trim().split(/\s+/) + const baseCommand = parts[0].toLowerCase() + + // Commands that read file contents + const fileReadingCommands = [ + // Unix commands + "cat", + "less", + "more", + "head", + "tail", + "grep", + "awk", + "sed", + // PowerShell commands and aliases + "get-content", + "gc", + "type", + "select-string", + "sls", + ] + + if (fileReadingCommands.includes(baseCommand)) { + // Check each argument that could be a file path + for (let i = 1; i < parts.length; i++) { + const arg = parts[i] + // Skip command flags/options (both Unix and PowerShell style) + if (arg.startsWith("-") || arg.startsWith("/")) { + continue + } + // Ignore PowerShell parameter names + if (arg.includes(":")) { + continue + } + // Validate file access + if (!this.validateAccess(arg)) { + return arg + } + } + } + + return undefined + } + /** * Filter an array of paths, removing those that should be ignored * @param paths - Array of paths to filter (relative to cwd) From b37f6887558399c1296c35ff6997f5097439aa45 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:11:14 -0800 Subject: [PATCH 20/24] Fix prompt responses --- src/core/Cline.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index f6ae751837..fee138284d 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1600,7 +1600,7 @@ export class Cline { const accessAllowed = this.clineIgnoreController.validateAccess(relPath) if (!accessAllowed) { await this.say("clineignore_error", relPath) - pushToolResult(formatResponse.clineIgnoreError(relPath)) + pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath))) await this.saveCheckpoint() break } @@ -1878,7 +1878,7 @@ export class Cline { const accessAllowed = this.clineIgnoreController.validateAccess(relPath) if (!accessAllowed) { await this.say("clineignore_error", relPath) - pushToolResult(formatResponse.clineIgnoreError(relPath)) + pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath))) await this.saveCheckpoint() break } @@ -2338,7 +2338,9 @@ export class Cline { const ignoredFileAttemptedToAccess = this.clineIgnoreController.validateCommand(command) if (ignoredFileAttemptedToAccess) { await this.say("clineignore_error", ignoredFileAttemptedToAccess) - pushToolResult(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)) + pushToolResult( + formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)), + ) await this.saveCheckpoint() break } From acd84e53a832b6dc3ed42b1076cf18e5b85aff3f Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 7 Feb 2025 15:31:09 -0800 Subject: [PATCH 21/24] fix tests; make sure .clineignore is ignored --- src/core/ignore/ClineIgnoreController.test.ts | 26 +++++-------------- src/core/ignore/ClineIgnoreController.ts | 1 + 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/core/ignore/ClineIgnoreController.test.ts b/src/core/ignore/ClineIgnoreController.test.ts index 084ad311c1..06f3d212ad 100644 --- a/src/core/ignore/ClineIgnoreController.test.ts +++ b/src/core/ignore/ClineIgnoreController.test.ts @@ -49,6 +49,11 @@ describe("ClineIgnoreController", () => { ] results.forEach((result) => result.should.be.true()) }) + + it("should block access to .clineignore file", async () => { + const result = controller.validateAccess(".clineignore") + result.should.be.false() + }) }) describe("Custom Patterns", () => { @@ -111,7 +116,7 @@ describe("ClineIgnoreController", () => { // ].join("\n"), // ) - // controller = new LLMFileAccessController(tempDir) + // controller = new ClineIgnoreController(tempDir) // const results = [ // // Basic negation @@ -194,25 +199,6 @@ describe("ClineIgnoreController", () => { const result = controller.validateAccess("src\\file.ts") result.should.be.true() }) - - it("should handle paths outside cwd", async () => { - // Create a path that points to parent directory of cwd - const outsidePath = path.join(path.dirname(tempDir), "outside.txt") - const result = controller.validateAccess(outsidePath) - - // Should return false for security since path is outside cwd - result.should.be.false() - - // Test with a deeply nested path outside cwd - const deepOutsidePath = path.join(path.dirname(tempDir), "deep", "nested", "outside.secret") - const deepResult = controller.validateAccess(deepOutsidePath) - deepResult.should.be.false() - - // Test with a path that tries to escape using ../ - const escapeAttemptPath = path.join(tempDir, "..", "escape-attempt.txt") - const escapeResult = controller.validateAccess(escapeAttemptPath) - escapeResult.should.be.false() - }) }) describe("Batch Filtering", () => { diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts index d3637caa76..ebd08788e7 100644 --- a/src/core/ignore/ClineIgnoreController.ts +++ b/src/core/ignore/ClineIgnoreController.ts @@ -69,6 +69,7 @@ export class ClineIgnoreController { const content = await fs.readFile(ignorePath, "utf8") this.clineIgnoreContent = content this.ignoreInstance.add(content) + this.ignoreInstance.add(".clineignore") } else { this.clineIgnoreContent = undefined } From 18e51dff2110886be453cd70f04d5c203d207550 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 8 Feb 2025 14:47:50 -0800 Subject: [PATCH 22/24] added explanatory markdown --- docs/cline-customization/clineignore.md | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/cline-customization/clineignore.md diff --git a/docs/cline-customization/clineignore.md b/docs/cline-customization/clineignore.md new file mode 100644 index 0000000000..2ef059b90b --- /dev/null +++ b/docs/cline-customization/clineignore.md @@ -0,0 +1,54 @@ +### .clineignore Support + +To give you more control over which files are accessible to Cline, we've implemented `.clineignore` functionality, similar to `.gitignore`. This allows you to specify files and directories that Cline should **not** access or process. This is useful for: + +* **Privacy:** Preventing Cline from accessing sensitive or private files in your workspace. +* **Performance:** Excluding large directories or files that are irrelevant to your tasks, potentially improving the efficiency of Cline. +* **Context Management:** Focusing Cline's attention on the relevant parts of your project. + +**How to use `.clineignore`** + +1. **Create a `.clineignore` file:** In the root directory of your workspace (the same level as your `.vscode` folder, or the top level folder you opened in VS Code), create a new file named `.clineignore`. + +2. **Define ignore patterns:** Open the `.clineignore` file and specify the patterns for files and directories you want Cline to ignore. The syntax is the same as `.gitignore`: + + * Each line in the file represents a pattern. + * **Standard glob patterns are supported:** + * `*` matches zero or more characters + * `?` matches one character + * `[]` matches a character range + * `**` matches any number of directories and subdirectories. + + * **Directory patterns:** Append `/` to the end of a pattern to specify a directory. + * **Negation patterns:** Start a pattern with `!` to negate (un-ignore) a previously ignored pattern. + * **Comments:** Start a line with `#` to add comments. + + **Example `.clineignore` file:** + + ``` + # Ignore log files + *.log + + # Ignore the entire 'node_modules' directory + node_modules/ + + # Ignore all files in the 'temp' directory and its subdirectories + temp/** + + # But DO NOT ignore 'important.log' even if it's in the root + !important.log + + # Ignore any file named 'secret.txt' in any subdirectory + **/secret.txt + ``` + +3. **Cline respects your `.clineignore`:** Once you save the `.clineignore` file, Cline will automatically recognize and apply these rules. + + * **File Access Control:** Cline will not be able to read the content of ignored files using tools like `read_file`. If you attempt to use a tool on an ignored file, Cline will inform you that access is blocked due to `.clineignore` settings. + * **File Listing:** When you ask Cline to list files in a directory (e.g., using `list_files`), ignored files and directories will still be listed, but they will be marked with a **🔒** symbol next to their name to indicate that they are ignored. This helps you understand which files Cline can and cannot interact with. + +4. **Dynamic Updates:** Cline monitors your `.clineignore` file for changes. If you modify, create, or delete your `.clineignore` file, Cline will automatically update its ignore rules without needing to restart VS Code or the extension. + +**In Summary** + +The `.clineignore` file provides a powerful and flexible way to control Cline's access to your workspace files, enhancing privacy, performance, and context management. By leveraging familiar `.gitignore` syntax, you can easily tailor Cline's focus to the most relevant parts of your projects. \ No newline at end of file From 1cc1c26585db6aa8bfd8a77fbf81915fb35ba430 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 8 Feb 2025 14:51:39 -0800 Subject: [PATCH 23/24] adding changeset --- .changeset/ten-books-act.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-books-act.md diff --git a/.changeset/ten-books-act.md b/.changeset/ten-books-act.md new file mode 100644 index 0000000000..f37fcf0e9a --- /dev/null +++ b/.changeset/ten-books-act.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Adding .clineignore guide From 407788ee642f9b075d45289255d02e8544f1ed6c Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 8 Feb 2025 14:57:16 -0800 Subject: [PATCH 24/24] remove old changeset --- .changeset/loud-countries-draw.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/loud-countries-draw.md diff --git a/.changeset/loud-countries-draw.md b/.changeset/loud-countries-draw.md deleted file mode 100644 index 3a6e55f337..0000000000 --- a/.changeset/loud-countries-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Add .clineignore file to block Cline from accessing specified file patterns \ No newline at end of file