Cline、プラグインとフックでループ拡張
Cline Blog は、LLM エージェントの非決定的なループを制御し、堅牢性を高めるためにプラグインとフックを活用する手法について解説している。
キーポイント
エージェントハネスの役割
Cline SDK はツール呼び出しやメッセージ履歴管理などの反復的な基盤を提供し、開発者はその上に独自の振る舞いを追加するプラグインを構築できる。
フックによる制御とガードレール
フックはループ内の固定ポイントで発火するコールバックであり、安全なツール呼び出し前や複雑な Git アクション前に実行することで、エージェントの行動を誘導・制限できる。
MCP サーバーとの明確な区別
MCP サーバーはモデルが要求した時にツールを提供するのに対し、プラグインのフックはモデルの要請に関わらずハネス内で自動的に実行され、強制力のあるガードレールとして機能する。
実装と環境要件
Cline CLI のインストール、認証設定、および Node または Bun 環境が必要であり、OpenAI や Anthropic などの既存プロバイダを連携してすぐに利用開始できる。
プラグインの構造と宣言
プラグインは名前、機能を示すマニフェスト(例:hooks のみ)、および初期化用の setup 関数を持つオブジェクトとして定義され、export default で Cline に自動認識されます。
フックによるループ拡張
エージェントの動作を監視・記録するために beforeRun や afterRun などのフックを使用し、ツール実行時の挙動変更には beforeTool フックを利用します。
スコープ管理と状態保持
setup 関数でコンテキストを読み込むが、フック自体はコンテキストを持たないため、パスや開始時刻などの値はモジュールスコープの変数として保持する必要があります。
重要な引用
An agent loop is a system around an LLM that lets it observe, decide, act, and repeat until a goal is met.
Hooks are code logic (callbacks) that fire at fixed points inside the agent loop.
MCP and plugins solve different problems. An MCP server exposes tools that the agent can call when the model decides it needs them.
A plugin is just an object
hooks is where the real behaviour is defined
Instead of relying on the model to enforce its own guardrails, a beforeRun hook applies them automatically before the action executes.
影響分析・編集コメントを表示
影響分析
この記事は、LLM エージェント開発において「自律性」と「制御」のバランスを取るための具体的なアーキテクチャパターンを示しており、実務レベルでの信頼性の高いエージェント構築に直結する知見を提供します。特に MCP との区別を明確にした点は、複雑化する AI エコシステムにおけるツール選定と設計判断において重要な指針となります。
編集コメント
LLM エージェントの実用化において最も課題となる「予測不能な挙動の制御」に対し、Cline が提供するフック機能は非常に実用的な解決策です。MCP との違いを明確に理解することで、より堅牢なシステム設計が可能になります。

エージェント・ループとは、LLM(大規模言語モデル)を取り巻くシステムで、目標を達成するまで「観測→判断→実行」のサイクルを繰り返す仕組みです。モデルは環境の状態を確認し、その目標に向かって次のアクションを決定します。
本稿では、この本質的に非決定的なループに、より確実な振る舞いと強力なガードレールをもたらすための有効なエージェント・ハッチ(枠組み)の構築方法を探ります。具体的には、プラグイン、特にフックを活用してエージェント・ループを制御・誘導する方法について解説します。
ループはすでに用意されている。残りはあなた次第です
Cline SDK が担うのは、エージェント・ループを取り巻く反復的で決定的な基盤部分です。ツール呼び出し、メッセージ履歴の管理、ターミナル出力の取得、ファイル編集、リトライ処理、停止条件など、必要な要素はすでに手厚く用意されています。
この土台が整った上で、次にやるべきはループの上に独自の振る舞いを追加することです。ここで活躍するのがプラグインです。
プラグインとは、Cline ハッチに接続する単一のファイル(オブジェクト)のことです。このファイル内では、ツールのラップ処理、プロバイダーの定義、メッセージの書き換え、自動化ロジック、そしてフックの実装などが可能です。一度作成したプラグインは、CLI 環境、VS Code、JetBrains IDE、および SDK を跨いで再利用できます。
本記事では、フックの活用方法について解説します。フックとは、エージェントループ内の固定ポイントで発火するコードロジック(コールバック)のことです。安全なツール呼び出しの前で実行するような単純なものから、Git 操作のような複雑な処理まで対応可能です。これにより、ループ全体を再構築することなく、2 つの重要な機能を実現できます。
具体的には、以下のことが可能になります。
- 何らかの動作を実行する(Watch what happened and store)
- 特定の動作を阻止する(Stop something from happening)
補足:MCP とプラグインは解決する課題が異なります。MCP サーバーは、モデルが必要と判断した際にエージェントが呼び出せるツールを公開します。一方、プラグインはエージェントハッチス内部で実行され、モデルが明示的に要求しなくても、エージェントループの特定のタイミングでフックが自動的に発火します。この違いは、強制力のあるガードレールが必要な場合に重要です。
始める前に準備するもの
以下の 3 つが必要です。
- Cline CLI のグローバルインストール(npm i -g cline)
- cline auth による認証設定
- TypeScript ファイルを実行するための Node または Bun
モデルについては、Cline に既に接続済みのものをそのまま使用してください。OpenAI、Anthropic、OpenRouter、あるいは API キーを自分で用意せずに高制限を利用したい場合は ClinePass がおすすめです。以下の例では cline auth から取得した providerId と modelId を使用しています。ご自身の値に置き換えてください。
ループの仕組み
プラグインを追加する前に、基本となるループの設定を確認しましょう。
SDK レベルでの構造は非常にシンプルです。ClineCore インスタンスを作成し、cline.start を呼び出します。その際、設定(モデル、ツール使用、システムプロンプト)とループ開始用のプロンプトを渡すだけで済みます。
import { ClineCore } from "@cline/core";
必ず JSON 形式で返してください。translation フィールドのみ。他のフィールドは一切追加しないこと — 余計なフィールドを書こうとして本文がトークン上限で打ち切られる事故を防ぐため:
{"translation": "リライト全文"}
const cline = await ClineCore.create({ backendMode: "local" });
const run = await cline.start({
config: {
providerId: "openai-codex",
modelId: "gpt-5.5",
cwd: process.cwd(),
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
},
prompt: "what files are in this folder?",
interactive: false,
});
console.log(run.result?.text);
await cline.dispose();There are only a few moving parts here.
providerId と modelId で Cline が使用するモデル設定を指定し、cwd で実行が属するワークスペースを定義します。systemPrompt と prompt はタスクの内容を決め、enableTools を有効にすると、エージェントは実行中にツールを呼び出せるようになります。
これが最小限かつ完全なハッチ(枠組み)の正体です。モデル側でどのツールを呼び出すか、あるいはどのアクションを実行するかを判断し、cline エンジンがそれを実行して結果を返します。このループは実行完了まで続きます。
このループ定義ができたら、次はそのループに独自のコードを接続していきます。
プラグインとは単なるオブジェクトです。
ループの定義が終わったので、いよいよプラグインを定義しましょう。
import type { AgentPlugin } from "@cline/core";
const plugin: AgentPlugin = {
name: "lifecycle-journal",
manifest: { capabilities: ["hooks"] },
setup(_api, ctx) {
ctx.logger?.log("[journal] plugin loaded");
},
hooks: {},
};
export default plugin;
name は識別子です。manifest で Cline にこのプラグインが何ができるかを伝えます。今回のケースでは、新しいツールやプロバイダーを追加するわけではないため、hooks の機能のみを要求しています。
プラグインの setup は、読み込み時に一度だけ実行されます。ここではワークスペース情報の取得やパスの設定を行い、プラグインが正常に起動したことをログ出力します。
hooks には実際の動作ロジックを定義します。詳細な設計は後ほど行います。
export default plugin を記述することで、Cline がこのプラグインを検出し、自動ロードできるようになります。マニフェストを確認し、自動的に接続される仕組みです。
基本骨格が整ったので、次は実際の核心ロジックに移ります。
実行前と実行後
フックには主に 2 つの役割があります。「監視(watch)」か「制御(steer)」です。前者は beforeRun や afterRun で動作し、後者は beforeTool で機能します。
まずはエージェントの挙動を変更せず、すべての実行記録をジャーナルに書き出すシンプルなフックから始めましょう。
ジャーナルの実装は単純で、実行開始時と終了時にタイムスタンプと所要時間を記録し、コスト情報も付与します。これにより、beforeRun と afterRun の 2 つのフックが必要になります。
まず、エージェントの領域にこのジャーナルを追加するためのコードを定義します:
import { appendFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import type { AgentPlugin } from "@cline/core";
let journalPath = join(process.cwd(), ".cline", "run-journal.jsonl");
let runStartedAt: number | undefined;
async function writeEntry(entry: Record): Promise {
await mkdir(dirname(journalPath), { recursive: true });
await appendFile(journalPath, ${JSON.stringify(entry)}\n);
}
setup では Cline のコンテキスト情報を蓄積しますが、hooks 自体はそれらを利用しません。
もしフックがセットアップ時の値(例えばワークスペースのパスなど)を必要とする場合、その値はモジュールスコープに保持します。これが journalPath や runStartedAt の役割です。
まずはプラグインの骨組みを作成しましょう。
const plugin: AgentPlugin = {
name: "lifecycle-journal",
manifest: { capabilities: ["hooks"] },
setup(_api, ctx) {
const root = ctx.workspaceInfo?.rootPath ?? process.cwd();
journalPath = join(root, ".cline", "run-journal.jsonl");
},
hooks: {
// フックはこの場所に記述します
},
};次に、最初のフックを定義します。
async beforeRun() {
runStartedAt = Date.now();
await writeEntry({
phase: "started",
at: new Date(runStartedAt).toISOString(),
});
return undefined;
}続いて、2 つ目のフックです。
async afterRun({ result }) {
const durationMs = runStartedAt !== undefined ? Date.now() - runStartedAt : undefined;
const { status, iterations, usage } = result;
await writeEntry({
phase: "finished",
durationMs,
status,
iterations,
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
totalCost: usage?.totalCost,
});
runStartedAt = undefined;
}これら両方のフックは、開始と終了の時刻を記録し、エントリを作成します。また、完了ステータス、イテレーション数、利用状況(トークン使用量など)を含む結果オブジェクトも定義しています。
実際の出力例は以下のようになります。
このフックは結果オブジェクトを単に追加するだけで、エージェントのループには介入しませんでした。
補足:afterRun は失敗または中止された実行でも発火します。そのため、すべての実行が成功したと仮定せず、ステータスを記録するようにしています。
実行前のコマンド停止
ジャーナルループの仕組みがわかったところで、次はアクションをトリガーするループに焦点を当てましょう。
beforeTool はツールの実行直前に発火し、危険だと判断された場合はツール自体の実行を中止する能力を持っています。これは、エージェントに rm -rf(Linux シェルの削除コマンド)のような破壊的なコマンドを実行させたくない場合に便利です。
高度なモデルであっても、文脈が不適切な場合、誤って安全でない、または意図しないアクションを引き起こすことがあります。モデル自身にガードレールを強制させるのではなく、beforeRun フックを使って実行前に自動的に適用するようにします。
ここでは、危険な破壊コマンドの実行を阻止するための beforeTool フックを実装しましょう。
まず、危険とみなすコマンドを定義します。
const DANGEROUS = [
/\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\b/i,
/\bgit\s+push\b[^\n]*\s(--force\b|-f\b)/i,
/\bmkfs(\.\w+)?\b/i,
/\bdd\b[^\n]*\bif=/i,
/:\(\)\s*\{\s*:\s*|\s*:/,
];
次に、フックを定義します。
const plugin: AgentPlugin = {
name: "tool-guard",
manifest: { capabilities: ["hooks"] },
hooks: {
async beforeTool({ toolCall, input }) {
if (toolCall.toolName !== "run_commands") return undefined;
const commands = extractShellCommands(input);
const blocked = commands.find((command) =>
DANGEROUS.some((pattern) => pattern.test(command)),
);
if (!blocked) return undefined;
return {
skip: true,
reason: Blocked run_commands: "${blocked}" looks destructive. Confirm with the user first.,
};
},
},
};
export default plugin;
このコードブロックは、tool-guard という名前のプラグイン(フック)を定義しています。これは beforeTool フックを提供するもので、ロジック自体は非常にシンプルです。エージェントがシェルコマンドを実行した際、その中に DANGEROOUS が定義している危険な文字列が含まれていれば即座にブロックし、そうでなければ処理を続行します。
SDK を通じて実行する
これで、実行履歴を記録するプラグインと、悪意のあるシェルコマンドをブロックするプラグインの 2 つが揃いました。これらを同時にテストするには、両方を同じ SDK コールの拡張機能として渡せばよいだけです。
import { ClineCore } from "@cline/core";
import journal from "./plugins/lifecycle-journal.ts";
import guard from "./plugins/tool-guard.ts";
const cline = await ClineCore.create({ backendMode: "local" });
const run = await cline.start({
config: {
providerId: "openai-codex",
modelId: "gpt-5.5",
cwd: process.cwd(),
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
extensions: [journal, guard], // here
},
prompt: "list the files here, then delete ./scratch-junk by running: rm -rf ./scratch-junk",
interactive: false,
});
console.log(run.result?.finishReason, run.result?.iterations);
await cline.dispose();
extensions: [journal, guard] register and load our plugin in Cline SDK for us.
Run it with:
npm install
npx tsx main.ts
output from that run looks something like this.
status : completed
iterations : 2
final text : I ran ls, but the second command was blocked by the tool guard
because rm -rf is destructive and requires confirmation.
And the journal filled that structurally at the same time.
{"phase":"started","at":"2026-07-09T09:45:24.987Z"}
{"phase":"finished","durationMs":4214,"status":"completed","iterations":2,"inputTokens":3351,"outputTokens":87,"totalCost":0.012453}
Same hooks, different rules
Once the hook is in place, the actual rule can be anything. You can play around with patterns and decide your own allow/deny list.
const DANGEROUS = [
/\bterraform\s+destroy\b/i,
/\bdrop\s+table\b/i,
/\bgit\s+push\b[^\n]*\s(main|master)\b/i,
];
The journal can change too. Instead of writing to .cline/run-journal.jsonl, afterRun could ping slack, push a desktop notification, or a cost metric.
必ずJSON形式で返してください。translation フィールドのみ。他のフィールドは一切追加しないこと — 余計なフィールドを書こうとして本文がトークン上限で打ち切られる事故を防ぐため:
{"translation": "リライト全文"}
async afterRun({ result }) {
await notify(run ${result.status}, cost $${result.usage?.totalCost});
}
hook body remains the same, the human deciding the loop(us) just mutates ideas.
What else you can hook
This post only used three of the seven hooks. The full set is fixed, and the same pattern works for all of them.
beforeRun / afterRun: wrap the whole run. Journaling, cost tracking, Slack pings.
beforeTool / afterTool: wrap a single tool call. Guards, audit logs, result rewrites.
beforeModel / afterModel: wrap a model call. Prompt shaping, redaction, response checks.
onEvent: stream runtime events as they happen. Live progress, metrics, debug traces.
A few places to start:
afterTool to log every tool call and its result
beforeModel to redact secrets from messages before the model call
onEvent to push live progress into a status bar or a webhook
The example repo has the two plugins from this post and the SDK runner. You can play around with them, with the logic of your choice.
Handing the keyboard back
Before this, a lot of agent safety and observability lived in your head.
Now that defined ruleset lives in the harness. The guard blocks the command on its own. The journal records the run on its own. You write the rule once, and every run after that carries it.
Cline already runs the agent. now you define hooks to watch it and fence it, with simple config it works on the CLI, VS Code, JetBrains, and the SDK.
プラグインと SDK ランナーの全コードは、例のリポジトリにあります。ルールを自分の環境に合わせて書き換えれば、ループが自動的に実行されます。
原文を表示
imageAn agent loop is a system around an LLM that lets it observe, decide, act, and repeat until a goal is met. The model looks at the state of the environment, and decides the action it needs to do, to iterate towards that goal.
In this blog, we’ll explore how to bring more deterministic behavior and stronger guardrails to this inherently non-deterministic loop by building an effective agent harness.
More specifically, we’ll look at how plugins, particularly hooks, can be used to steer and control the agent loop.
The loop is served on a silver platter, the rest is up to you
Cline SDK handles the repetitive, deterministic scaffolding around the agent loop. Essentials like tool calling, message history, terminal output, file edits, retries, stop conditions etc are already gracefully handled.
Once that foundational layer is in place, the next step is to add your own behavior on top of the loop. That is where plugins help.
A plugin is one file, a single object, that you attach to the Cline harness. Inside that file you can wrap tools, providers, message rewriters, automations, or hooks. A plugin once written can be reused across the CLI, VS Code, JetBrains, and the SDK.
This post will teach you how to play around with hooks. Hooks are code logic (callbacks) that fire at fixed points inside the agent loop. It can be as trivial as running it before a safe tool call or a complex git action which is enough for us to do two useful things without rebuilding the whole loop.
Watch what happened and store
Stop something from happening
Short note: MCP and plugins solve different problems. An MCP server exposes tools that the agent can call when the model decides it needs them. A plugin runs inside the agent harness, where its hooks can execute automatically at specific points in the agent loop, whether or not the model explicitly asks for them. That distinction matters when you need an enforceable guardrail.
Before you start
You need three things.
Cline CLI installed globally with npm i -g cline
Auth configured with cline auth
Node or Bun to run the TypeScript files
For the model, use whatever you already have wired into Cline. OpenAI, Anthropic, OpenRouter, or ClinePass if you want high limits without bringing your own key. The examples below use providerId and modelId from cline auth. Swap them for your own values.
What the loop looks like
Before adding a plugin lets take a look at configuring the fundamental loop.
At the SDK level, the shape is dead simple. Create a ClineCore instance, call cline.start, give it a config(model , tool use , and system prompt) and the prompt to start the loop.
import { ClineCore } from "@cline/core";
const cline = await ClineCore.create({ backendMode: "local" });
const run = await cline.start({
config: {
providerId: "openai-codex",
modelId: "gpt-5.5",
cwd: process.cwd(),
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
},
prompt: "what files are in this folder?",
interactive: false,
});
console.log(run.result?.text);
await cline.dispose();There are only a few moving parts here.
providerId and modelId tell Cline which model setup to use, cwd tells it what workspace the run belongs to, systemPrompt and prompt define the task, enableTools lets the agent call tools during the run.
This is what makes this a minimal and complete harness, model can decide what tool to call or what action to steer, cline engine will run it and return the result back till the run is complete.
Once the loop is defined, we attach our own code to that loop.
A plugin is just an object
Loop is out of the way now we define the plugin:
import type { AgentPlugin } from "@cline/core";
const plugin: AgentPlugin = {
name: "lifecycle-journal",
manifest: { capabilities: ["hooks"] },
setup(_api, ctx) {
ctx.logger?.log("[journal] plugin loaded");
},
hooks: {},
};
export default plugin;
The name is identifier. The manifest tells Cline what this plugin is capable of. In our case, we only ask for hooks, because we are not adding a new tool or provider here.
setup runs once when the plugin loads, place where you read workspace information, set up paths, or log that the plugin is alive.
hooks is where the real behaviour is defined , which we will design later.
export default plugin makes the plugin visible for cline to auto load , see the manifest and wire it automatically.
The basic skeleton is in place, now we move to the actual core logic.
Before the run, after the run
Hook generally does two broad things , either watch (beforeRun afterRun) or steer (beforeTool).
We can start with a simple hook, that records everything, run to a journal and does not mutate the agent behaviour as a whole.
A Journal is simple - write when the run started , when it finished and the time delta with the cost attached, which gives us two hooks -
beforeRun
afterRun
Firstly, we will define the agents space to append the journal:
import { appendFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import type { AgentPlugin } from "@cline/core";
let journalPath = join(process.cwd(), ".cline", "run-journal.jsonl");
let runStartedAt: number | undefined;
async function writeEntry(entry: Record<string, unknown>): Promise<void> {
await mkdir(dirname(journalPath), { recursive: true });
await appendFile(journalPath, ${JSON.stringify(entry)}\n);
}
setup accumulates the Cline context but the hooks themselves do not.
So if a hook needs a value from setup, like the workspace path, we keep it in module scope. That is what journalPath and runStartedAt are for .
Now we carve the plugin scaffolding first:
const plugin: AgentPlugin = {
name: "lifecycle-journal",
manifest: { capabilities: ["hooks"] },
setup(_api, ctx) {
const root = ctx.workspaceInfo?.rootPath ?? process.cwd();
journalPath = join(root, ".cline", "run-journal.jsonl");
},
hooks: {
// hooks go here
},
};
Now, we define the first hook:
async beforeRun() {
runStartedAt = Date.now();
await writeEntry({
phase: "started",
at: new Date(runStartedAt).toISOString(),
});
return undefined;
}
and then the second hook:
async afterRun({ result }) {
const durationMs = runStartedAt !== undefined ? Date.now() - runStartedAt : undefined;
const { status, iterations, usage } = result;
await writeEntry({
phase: "finished",
durationMs,
status,
iterations,
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
totalCost: usage?.totalCost,
});
runStartedAt = undefined;
}
Both of them note down the time (start/finish), do the entry, define the result object which carries the post status, iteration count and usage.
This is what our output might look like:
{"phase":"started","at":"2026-07-09T09:08:56.125Z"}
{"phase":"finished","durationMs":8806,"status":"completed","iterations":2,"inputTokens":7994,"outputTokens":198,"totalCost":0.029782}
This hook only appended its result object, staying out of the agent loop.
Short note: afterRun can fire for failed or aborted runs too. That is why we record status instead of assuming every run completed successfully.
Stop the command before it runs
Now we know how journal loops work, we now focus on the ones that trigger an action.
beforeTool runs right before a tool execution, it has the ability to terminate the tool action itself if it sees it as dangerous. This can be handy in situation where we don't want agent to perform destructive commands like rm -rf (delete commands in linux shell).
Even a highly capable model can sometimes be thrown off by poor context and trigger an unsafe or unintended action. Instead of relying on the model to enforce its own guardrails, a beforeRun hook applies them automatically before the action executes.
We will write a beforeTool hook for it to stop from running dangerous destructive commands.
First we define the commands we deem dangerous:
const DANGEROUS = [
/\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\b/i,
/\bgit\s+push\b[^\n]*\s(--force\b|-f\b)/i,
/\bmkfs(\.\w+)?\b/i,
/\bdd\b[^\n]*\bif=/i,
/:\(\)\s*\{\s*:\s*\|\s*:/,
];
Then we define the hook:
const plugin: AgentPlugin = {
name: "tool-guard",
manifest: { capabilities: ["hooks"] },
hooks: {
async beforeTool({ toolCall, input }) {
if (toolCall.toolName !== "run_commands") return undefined;
const commands = extractShellCommands(input);
const blocked = commands.find((command) =>
DANGEROUS.some((pattern) => pattern.test(command)),
);
if (!blocked) return undefined;
return {
skip: true,
reason: Blocked run_commands: "${blocked}" looks destructive. Confirm with the user first.,
};
},
},
};
export default plugin;
This code block defines a plugin (hook) called tool-guard, which provisions a beforeTool hook. The logic is really simple, if the agent executes a shell command, and that happens to have the dangerous strings DANGEROUS defines, then immediately block, else continue.
Run it through the SDK
Now we have two plugins: one that journals the run, and one that blocks a bad shell command. To test them together, we pass both as extensions in the same SDK call.
import { ClineCore } from "@cline/core";
import journal from "./plugins/lifecycle-journal.ts";
import guard from "./plugins/tool-guard.ts";
const cline = await ClineCore.create({ backendMode: "local" });
const run = await cline.start({
config: {
providerId: "openai-codex",
modelId: "gpt-5.5",
cwd: process.cwd(),
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
extensions: [journal, guard], // here
},
prompt: "list the files here, then delete ./scratch-junk by running: rm -rf ./scratch-junk",
interactive: false,
});
console.log(run.result?.finishReason, run.result?.iterations);
await cline.dispose();
extensions: [journal, guard] register and load our plugin in Cline SDK for us.
Run it with:
npm install
npx tsx main.ts
output from that run looks something like this.
status : completed
iterations : 2
final text : I ran ls, but the second command was blocked by the tool guard
because rm -rf is destructive and requires confirmation.
And the journal filled that structurally at the same time.
{"phase":"started","at":"2026-07-09T09:45:24.987Z"}
{"phase":"finished","durationMs":4214,"status":"completed","iterations":2,"inputTokens":3351,"outputTokens":87,"totalCost":0.012453}
Same hooks, different rules
Once the hook is in place, the actual rule can be anything. You can play around with patterns and decide your own allow/deny list.
const DANGEROUS = [
/\bterraform\s+destroy\b/i,
/\bdrop\s+table\b/i,
/\bgit\s+push\b[^\n]*\s(main|master)\b/i,
];
The journal can change too. Instead of writing to .cline/run-journal.jsonl, afterRun could ping slack, push a desktop notification, or a cost metric.
async afterRun({ result }) {
await notify(run ${result.status}, cost $${result.usage?.totalCost});
}
hook body remains the same, the human deciding the loop(us) just mutates ideas.
What else you can hook
This post only used three of the seven hooks. The full set is fixed, and the same pattern works for all of them.
beforeRun / afterRun: wrap the whole run. Journaling, cost tracking, Slack pings.
beforeTool / afterTool: wrap a single tool call. Guards, audit logs, result rewrites.
beforeModel / afterModel: wrap a model call. Prompt shaping, redaction, response checks.
onEvent: stream runtime events as they happen. Live progress, metrics, debug traces.
A few places to start:
afterTool to log every tool call and its result
beforeModel to redact secrets from messages before the model call
onEvent to push live progress into a status bar or a webhook
The example repo has the two plugins from this post and the SDK runner. You can play around with them, with the logic of your choice.
Handing the keyboard back
Before this, a lot of agent safety and observability lived in your head.
Now that defined ruleset lives in the harness. The guard blocks the command on its own. The journal records the run on its own. You write the rule once, and every run after that carries it.
Cline already runs the agent. now you define hooks to watch it and fence it, with simple config it works on the CLI, VS Code, JetBrains, and the SDK.
The full code for both plugins and the SDK runner is in the example repo. Swap the
rules for your own, and let the loop run itself.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み