· 7 min read
Building Custom Agents With the Cline SDK: Start With the Tool Boundary
By N. De Vries
- tools
Use the Cline SDK if you need an agent to do work through your application—not merely chat about it—and you’re willing to own its tool boundary. For a custom coding agent, start with ClineCore, give it one narrow capability, and require approval for everything that can write, run, deploy, or leak data.
That recommendation is deliberately less exciting than “build an autonomous engineer.” The SDK already gives you an agent loop, provider wiring, sessions, built-in workspace tools, streaming events, and extension points. The part that decides whether your agent is useful on Monday is the 30 lines around the model: which directory it can touch, which tool results it sees, and what happens when it asks to run npm publish.
Pick ClineCore unless you really mean “custom tools only”
@cline/sdk re-exports the lower-level packages, including the direct Agent runtime and ClineCore. The direct runtime is the smaller option: you supply every tool yourself. That’s a good fit for a Slack bot that can search an internal API, or a PR labeler that can only read a diff. It is not the easiest starting point for a coding agent, because it does not bring Cline’s file, search, editor, shell, web-fetch, session, and workspace machinery with it.
For a repo-aware agent, use ClineCore. Set cwd explicitly. If you omit both cwd and workspaceRoot, the runtime uses Cline’s shared chat workspace instead of the repository you probably thought you were targeting. That is a surprisingly easy way to test a capable agent and learn nothing about whether it works in the project you care about.
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({
clientName: "release-note-bot",
capabilities: {
requestToolApproval: async (request) => {
console.log(request.toolName, request.input)
return { approved: false }
},
},
})
const session = await cline.start({
prompt: "Read recent merged PRs and draft release notes in RELEASE_NOTES.md.",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: process.cwd(),
workspaceRoot: process.cwd(),
enableTools: true,
systemPrompt: "Draft concise release notes. Do not modify source code.",
},
toolPolicies: {
read_files: { autoApprove: true },
search_codebase: { autoApprove: true },
apply_patch: { autoApprove: false },
editor: { enabled: false },
run_commands: { autoApprove: false },
fetch_web_content: { enabled: false },
},
})
console.log(session.result?.text)Run that in a disposable checkout first. It is intentionally boring: reads and code search proceed; patches pause; direct editor access and web access do not exist from the model’s perspective. The Cline permission model matters here because tools with no explicit policy default to enabled and auto-approved. Treat an omitted policy as a bug, not a convenience.
Build one tool that removes real friction
A custom agent earns its keep when it crosses a boundary that a general coding tool cannot cross cleanly: looking up a feature flag, collecting deployment status, querying an internal ownership catalog, or opening a pre-filled incident ticket. Don’t begin by exposing a generic internal HTTP client. Make the model choose from verbs your team recognizes.
For example, a deployment assistant should get get_deployment, deploy_preview, and perhaps request_production_deploy. It should not get call_internal_api with arbitrary URL, method, headers, and body. The latter saves you implementation time and spends it again in prompt ambiguity, bad authorization paths, and reviews of tool-call logs.
import { createTool } from "@cline/sdk"
import { z } from "zod"
const deploymentStatus = createTool({
name: "get_deployment_status",
description: "Return the current deployment state for a named service in staging.",
inputSchema: z.object({
service: z.enum(["api", "web", "worker"]),
}),
async execute({ service }) {
const deployment = await platform.deployments.latest({
service,
environment: "staging",
})
return {
service,
state: deployment.state,
commit: deployment.commitSha,
url: deployment.url,
}
},
})The schema is not decoration. It constrains arguments before your code executes, while the description tells the model when the tool is appropriate. Return small structured results, not a 400 KB JSON response or an HTML page dumped into context. If the agent needs logs, add a second tool with a fixed maximum—say, the last 100 lines—and redact tokens before returning them.
Make approvals a product feature, not a terminal prompt
The SDK supports a requestToolApproval callback, so your application can turn a tool call into a UI card, a Slack button, or a CI status check. Use it. A useful policy is to auto-approve reading repository files and searching code, then ask for confirmation on patches, shell commands, network requests, and every irreversible business action.
Approval by tool name is only the first pass. A run_commands permission covers both git status and git push --force. Put conditional policy in the approval callback: allow a short read-only command allowlist, require an explicit human decision for everything else, and record the request input with the decision. Don’t rely on the system prompt to enforce that distinction; prompts tell the model what you prefer, while the callback determines what actually runs.
Use events to make a run debuggable
A custom agent without an event trail becomes a support problem immediately. Subscribe to the session and persist enough to answer basic questions: which model ran, which tools it called, which ones were denied, how long it took, and why it stopped. ClineCore exposes session IDs, persisted messages, accumulated usage, abort/stop methods, and event subscription. That is enough to build a run page before you build any multi-agent architecture.
Start by logging the event type and session ID to stdout. Then add an internal record keyed by sessionId. If a run starts making bad edits, call abort(sessionId, reason) rather than hoping a later prompt will steer it back. That sounds mundane, but cancellation, replayable evidence, and tool-call visibility are what separate an integration from a demo.
Don’t add subagents or plugins on day one
Cline has examples for plugins, lifecycle hooks, scheduled automations, and background subagents. Those are useful once a single agent has a stable job. They are also a fine way to multiply unclear responsibilities. If you cannot state the input, allowed tools, completion condition, and rollback behavior of one agent in a short README, three specialized agents will produce more expensive ambiguity.
There are two specific traps to avoid. First, a plugin or hook is not automatically a security boundary: use runtime hooks to block or redact, but keep the underlying tool implementation least-privileged. Second, auto-approving all tools is documented as suitable for sandboxed or fully trusted environments. A developer laptop with cloud credentials, SSH agents, and a checked-out production configuration is neither.
The first version worth shipping is narrow: one repository, one repeatable task, read access by default, one or two custom tools, visible approvals, and a hard stop. Once that run is boring—and someone can explain every tool call after the fact—add the scheduled trigger, the Slack surface, or the planning subagent. The SDK can support all of them. Your job is to make sure it has earned them.