ToolValidator and AutoApprove: Two Gates Before Execution
Responsibilities
Before a tool actually runs, two independent checks fire: ToolValidator enforces parameter and path legality, and AutoApprove decides whether to pop an approval dialog. Neither executes the tool — they only decide "can it run" and "should the user be asked".
ToolValidator is a very thin utility class for handlers. It exposes two methods: assertRequiredParams checks that required parameters are present, and checkClineIgnorePath checks whether a path is blocked by .clineignore (assertRequiredParams:17). It has no side effects — it just returns a ValidationResult and lets the handler decide what to do.
AutoApprove is slightly more involved. It is a private field of ToolExecutor, injected into TaskConfig via asToolConfig (autoApprover init:126). Handlers reach it indirectly through config.callbacks.shouldAutoApproveTool or config.callbacks.shouldAutoApproveToolWithPath (callback wiring:181). It reads three global switches — yoloModeToggled, autoApproveAllToggled, autoApprovalSettings — plus whether the path is inside the workspace, and produces one of three outcomes: "local auto-approve", "external auto-approve", or "no auto-approve".
Design motivation
- Validator and AutoApprove are separated: legality checks and approval are orthogonal concerns — "parameters valid but user has not approved" and "parameters invalid but user already approved" can both occur. Splitting them into two objects means a handler need not skip approval on an error path, and can still block invalid parameters even after approval passes.
- Validator only looks, never mutates: it returns a
ValidationResultrather than throwing, so the handler decides whether to push an error result or fall back to a degraded path (ValidationResult:4). Different handlers can then share the same error display style. - Three tiers of AutoApprove switches:
yololets everything through,autoApproveAllis the next-broader scope, andautoApprovalSettingsis the most granular (distinguishing local/external, safe commands/all commands) (shouldAutoApproveTool:42). The broader switch wins. - Path classification gates external access: for read/write tools the auto-approve result is a
[local, external]tuple (tuple result:96).localcontrols files inside the workspace,externalcontrols files outside. By default external is stricter, to prevent accidental edits to system files. - Workspace info is cached at task scope:
getWorkspaceInfois fetched once and cached inworkspacePathsCacheandisMultiRootScenarioCache(getWorkspaceInfo:23). Within a task's lifetime the workspace roots rarely change, so caching avoids repeated IPC calls.
Key files
ToolValidator:10— class definition; constructed withClineIgnoreController.assertRequiredParams:17— iterates parameter names; returns an error for null or whitespace-only values.checkClineIgnorePath:32— callsclineIgnoreController.validateAccess; returns an error when access is blocked.AutoApprove:8— class definition; holdsstateManagerand two cache fields.getWorkspaceInfo:23— cached workspace paths and the multi-root flag.shouldAutoApproveTool:42— three-tier switch: YOLO, autoApproveAll, fine-grained autoApprovalSettings.yolo branch:43— under YOLO, read/file operations return[true, true], while browser/web/MCP returntrue.autoApproveAll branch:66— same scope as YOLO but as a secondary switch.autoApprovalSettings:88— the fine-grained branch, broken down by actions.readFiles / editFiles / executeSafeCommands etc.shouldAutoApproveToolWithPath:122— the path-aware variant: classifies local/external, then combines with the auto-approve tuple.multi-root workspace check:138— in multi-root scenarios,isLocatedInWorkspacechecks any root.final decision:163— onlylocal && autoApproveLocalorexternal && autoApproveLocal && autoApproveExternalis approved.ToolExecutor delegations:48— ToolExecutor binds itself as the callback, forwarding toautoApprover.complete block approval:205— the complete block goes throughshouldAutoApproveToolWithPathto choose auto-approval or a manualask("tool", ...).partial block approval:74— the streaming partial block runs the same auto-approve check, deciding whether to pop the dialog mid-stream.
Data flow
After receiving a block, a handler typically uses Validator to check parameters first, then AutoApprove to pick the UI path. Here is the actual call WriteToFileToolHandler makes during the partial-block stage:
// apps/vscode/src/core/task/tools/handlers/WriteToFileToolHandler.ts
// Handle auto-approval vs manual approval for partial
if (await uiHelpers.shouldAutoApproveToolWithPath(block.name, relPath)) {
await uiHelpers.removeLastPartialMessageIfExistsWithType("ask", "tool") // in case the user changes auto-approval settings mid stream
await uiHelpers.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
await uiHelpers.removeLastPartialMessageIfExistsWithType("say", "tool")
await uiHelpers.ask("tool", partialMessage, block.partial).catch(() => {})
}Inside shouldAutoApproveToolWithPath, the two master switches — YOLO and autoApproveAll — are checked first; if either is on, the function returns true immediately (yolo short circuit:126). It then determines whether the path is inside the workspace and combines that with the tuple from shouldAutoApproveTool to produce the final decision:
// apps/vscode/src/core/task/tools/autoApprove.ts
const autoApproveResult = this.shouldAutoApproveTool(blockname)
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
? autoApproveResult
: [autoApproveResult, false]
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
return true
}
return falseHere autoApproveExternal is only checked when autoApproveLocal is also true — meaning "auto-approve external" implicitly requires "auto-approve local". This is Cline's default safety posture: external operations are always one tier stricter than local ones.
Boundaries and failure modes
- Empty strings count as missing:
assertRequiredParamsusesString(val).trim() === "", so empty strings and whitespace-only strings all count as missing (empty check:21). But thecontentfield is allowed to be an empty string (creating an empty file), so the handler judges that itself with== nulland does not go through Validator (content null check:42). - No path defaults to no approval: when
shouldAutoApproveToolWithPathreceives anundefinedpath, it setsisLocalRead = falsedirectly (no path default:153). The safe-side default — a tool without a path is treated as an external operation. - The cache assumes the workspace does not change: the comment says it outright — "assumes that the task has a fixed set of workspace roots" (
cache assumption:11). If the user adds a new root mid-task, this task still sees the old cache; the new root only takes effect for the next task. - YOLO does not cover every tool: the YOLO switch only lists read/write/bash/browser/web/MCP;
ASK,ATTEMPT, andNEW_TASKare not listed (yolo switch list:44). These tools fall through toreturn falseand require normal approval. - The YOLO tuple only applies to file/bash tools: the
[true, true]tuple is only returned for read/write/bash/subagents (yolo tuple:55). browser/web/MCP receive a scalartrue, meaning that under YOLO they do not distinguish local from external. - Validator is not used by the legacy ToolExecutor: the class comment says it explicitly — "The legacy ToolExecutor switch remains unchanged and does not depend on this" (
legacy note:8). In the legacy path, parameter checks are scattered across the case branches; Validator only serves the new handlers.
Summary
Validator is the hard check on parameters and paths — on failure it surfaces an error and lets the model retry. AutoApprove is a soft, user-preference-driven switch that decides between auto-approval and a dialog. Together they act as gatekeepers before a tool executes; handlers call them and then decide the next move. To see how the handler list is registered, see /tools/handlers-overview; for the router itself, see /tools/coordinator.