Check .clineignore before executing edit and read operations

This commit is contained in:
Matt Rubens 2025-01-25 21:40:14 -05:00
parent 5d19bbd52a
commit 2b988a00ce
3 changed files with 89 additions and 10 deletions

View file

@ -1,7 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { loadClineIgnoreFile } from "../utils/cline-ignore"
import cloneDeep from "clone-deep"
import { DiffStrategy, getDiffStrategy, UnifiedDiffStrategy } from "./diff/DiffStrategy"
import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validator"
import { isToolAllowedForMode, ToolName } from "./mode-validator"
import delay from "delay"
import fs from "fs/promises"
import os from "os"
@ -76,6 +77,7 @@ export class Cline {
private urlContentFetcher: UrlContentFetcher
private browserSession: BrowserSession
private didEditFile: boolean = false
private ignoreContent: string = ""
customInstructions?: string
diffStrategy?: DiffStrategy
diffEnabled: boolean = false
@ -132,6 +134,9 @@ export class Cline {
this.providerRef = new WeakRef(provider)
this.diffViewProvider = new DiffViewProvider(cwd)
// Load ignore content once at initialization
loadClineIgnoreFile(cwd).then((content) => (this.ignoreContent = content))
if (historyItem) {
this.taskId = historyItem.id
}
@ -1149,15 +1154,17 @@ export class Cline {
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
try {
validateToolUse(
const isAllowed = isToolAllowedForMode(
block.name as ToolName,
mode ?? defaultModeSlug,
customModes ?? [],
{
apply_diff: this.diffEnabled,
},
{ apply_diff: this.diffEnabled },
block.params,
this.ignoreContent,
)
if (!isAllowed) {
throw new Error(`Tool "${block.name}" is not allowed in ${mode ?? defaultModeSlug} mode.`)
}
} catch (error) {
this.consecutiveMistakeCount++
pushToolResult(formatResponse.toolError(error.message))
@ -2718,9 +2725,7 @@ export class Cline {
// Add warning if not in code mode
if (
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], {
apply_diff: this.diffEnabled,
}) &&
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], { apply_diff: this.diffEnabled }) &&
!isToolAllowedForMode("apply_diff", currentMode, customModes ?? [], { apply_diff: this.diffEnabled })
) {
const currentModeName = getModeBySlug(currentMode, customModes)?.name ?? currentMode

View file

@ -1,4 +1,4 @@
import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
import { isToolAllowedForMode, FileRestrictionError, IgnoredFileError, ModeConfig } from "../modes"
describe("isToolAllowedForMode", () => {
const customModes: ModeConfig[] = [
@ -186,3 +186,47 @@ describe("FileRestrictionError", () => {
expect(error.name).toBe("FileRestrictionError")
})
})
describe("IgnoredFileError", () => {
it("formats error message correctly", () => {
const error = new IgnoredFileError("test.js")
expect(error.message).toBe("File test.js is ignored by .clineignore")
expect(error.name).toBe("IgnoredFileError")
})
})
describe("clineignore handling", () => {
const ignoreContent = "*.js\n!important.js\nnode_modules/"
it("throws IgnoredFileError for ignored files in read group", () => {
expect(() =>
isToolAllowedForMode("read_file", "code", [], undefined, { path: "test.js" }, ignoreContent),
).toThrow(IgnoredFileError)
expect(() =>
isToolAllowedForMode("read_file", "code", [], undefined, { path: "node_modules/test.ts" }, ignoreContent),
).toThrow(IgnoredFileError)
})
it("throws IgnoredFileError for ignored files in edit group", () => {
expect(() =>
isToolAllowedForMode("write_to_file", "code", [], undefined, { path: "test.js" }, ignoreContent),
).toThrow(IgnoredFileError)
expect(() =>
isToolAllowedForMode("apply_diff", "code", [], undefined, { path: "node_modules/test.ts" }, ignoreContent),
).toThrow(IgnoredFileError)
})
it("allows non-ignored files", () => {
expect(isToolAllowedForMode("read_file", "code", [], undefined, { path: "test.ts" }, ignoreContent)).toBe(true)
expect(
isToolAllowedForMode("write_to_file", "code", [], undefined, { path: "important.js" }, ignoreContent),
).toBe(true)
})
it("allows tools without paths", () => {
expect(isToolAllowedForMode("browser_action", "code", [], undefined, undefined, ignoreContent)).toBe(true)
})
})

View file

@ -1,4 +1,5 @@
import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
const { shouldIgnorePath } = require("../utils/cline-ignore")
// Mode types
export type Mode = string
@ -154,12 +155,31 @@ export class FileRestrictionError extends Error {
}
}
// Custom error class for ignored files
export class IgnoredFileError extends Error {
constructor(filePath: string) {
super(`File ${filePath} is ignored by .clineignore`)
this.name = "IgnoredFileError"
}
}
/**
* Checks if a tool is allowed for a given mode, including .clineignore restrictions
* @param tool The tool name to check
* @param modeSlug The mode slug to check against
* @param customModes Array of custom mode configurations
* @param toolRequirements Optional map of tool-specific requirements
* @param toolParams Optional parameters passed to the tool
* @param ignoreContent Optional .clineignore file content
* @returns true if tool is allowed, throws error otherwise
*/
export function isToolAllowedForMode(
tool: string,
modeSlug: string,
customModes: ModeConfig[],
toolRequirements?: Record<string, boolean>,
toolParams?: Record<string, any>, // All tool parameters
toolParams?: Record<string, any>,
ignoreContent?: string,
): boolean {
// Always allow these tools
if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
@ -188,6 +208,16 @@ export function isToolAllowedForMode(
continue
}
// Check .clineignore for read/edit groups
if (
toolParams?.path &&
ignoreContent &&
(groupName === "read" || groupName === "edit") &&
shouldIgnorePath(toolParams.path, ignoreContent)
) {
throw new IgnoredFileError(toolParams.path)
}
// If there are no options, allow the tool
if (!options) {
return true