Permission Handling
Control which tools agents can run automatically and which require explicit approval.
Tool policies control whether tools are enabled and whether they run without approval. Tool names not listed in toolPolicies default to enabled and auto-approved, so set policies explicitly for tools that need review.
Tool Policies
The simplest way to manage permissions is through tool policies. Set them per-tool when creating an agent:
import { Agent, createBuiltinTools } from "@cline/sdk"
const agent = new Agent({
// ...config
tools: createBuiltinTools({ cwd: process.cwd() }),
toolPolicies: {
read_files: { autoApprove: true }, // Always run without asking
search_codebase: { autoApprove: true }, // Always run without asking
editor: { autoApprove: false }, // Always ask before running
run_commands: { autoApprove: false }, // Always ask before running
},
})Policy Options
| Policy | Effect |
|---|---|
{ autoApprove: true } | Tool executes immediately without approval |
{ autoApprove: false } | Tool waits for approval before executing |
{ enabled: false } | Tool is completely disabled (model won't see it) |
| No policy set | Defaults to enabled and auto-approved |
Auto-Approve Everything
For trusted environments (CI pipelines, sandboxed containers, development scripts):
const agent = new Agent({
// ...config
tools: allTools,
toolPolicies: Object.fromEntries(
allTools.map((t) => [t.name, { autoApprove: true }])
),
})Or with ClineCore:
const session = await cline.start({
config: {
enableTools: true,
},
toolPolicies: {
run_commands: { autoApprove: true },
editor: { autoApprove: true },
read_files: { autoApprove: true },
apply_patch: { autoApprove: true },
search_codebase: { autoApprove: true },
fetch_web_content: { autoApprove: true },
},
capabilities: {
requestToolApproval: async () => ({ approved: true }),
},
// ...
})Auto-approving all tools means the agent can execute any shell command, modify any file, and make network requests without your review. Only use this in environments where the agent's actions are either sandboxed or fully trusted.
Interactive Approval
For applications that need human oversight, implement a custom approval handler:
import { ClineCore } from "@cline/sdk"
import * as readline from "readline"
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
const ask = (q: string) => new Promise<string>((res) => rl.question(q, res))
const cline = await ClineCore.create({
clientName: "interactive-app",
capabilities: {
requestToolApproval: async (request) => {
console.log(`\nTool: ${request.toolName}`)
console.log(`Input: ${JSON.stringify(request.input, null, 2)}`)
const answer = await ask("Approve? (y/n): ")
return { approved: answer.toLowerCase() === "y" }
},
},
})Tiered Permissions
A practical middle ground: auto-approve read-only operations, require approval for writes:
const READ_TOOLS = new Set(["read_files", "search_codebase", "fetch_web_content"])
const WRITE_TOOLS = new Set(["run_commands", "editor", "apply_patch"])
const toolPolicies: Record<string, { autoApprove: boolean }> = {}
for (const tool of READ_TOOLS) {
toolPolicies[tool] = { autoApprove: true }
}
for (const tool of WRITE_TOOLS) {
toolPolicies[tool] = { autoApprove: false }
}Conditional Approval Logic
Approve based on what the tool is actually doing, not just which tool it is:
const cline = await ClineCore.create({
clientName: "smart-approval",
capabilities: {
requestToolApproval: async (request) => {
// request.input is the model's raw tool input, so check its shape first.
const input = request.input as { commands?: unknown; files?: unknown }
// Auto-approve a fixed set of commands that only read: { commands: string[] }.
// Match whole commands, because flags can change what a command does.
// Git can also run programs named in a repository's config (for example
// core.fsmonitor), so approve Git commands only in repositories you trust.
if (request.toolName === "run_commands" && Array.isArray(input.commands)) {
const safeCommands = new Set(["ls", "git status", "git log --oneline -20"])
if (input.commands.every((cmd) => typeof cmd === "string" && safeCommands.has(cmd.trim()))) {
return { approved: true }
}
}
// Auto-approve reads in specific directories: { files: { path: string }[] }.
if (request.toolName === "read_files" && Array.isArray(input.files)) {
const isAllowed = (file: unknown) => {
const path = (file as { path?: unknown })?.path
return typeof path === "string" && !path.includes("..") &&
(path.startsWith("/src/") || path.startsWith("/tests/"))
}
if (input.files.every(isAllowed)) {
return { approved: true }
}
}
// Everything else requires manual approval
console.log(`Approval needed: ${request.toolName}`)
console.log(`Input: ${JSON.stringify(request.input)}`)
return { approved: false }
},
},
})What Happens When a Tool is Rejected
When approval is denied, the agent receives a rejection message and can adjust its approach. It might:
- Ask the user for clarification
- Try a different tool to accomplish the same goal
- Modify its approach and try again with different parameters
- Give up on that subtask and move on
The agent does not get stuck in a loop. The rejection counts as a response, and the agent proceeds with its next iteration.