Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions plugins/cline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# GoPlus AgentGuard — Cline plugin

A native [Cline](https://github.com/cline/cline) plugin that runs every tool
call through the [GoPlus AgentGuard](https://github.com/GoPlusSecurity/agentguard)
decision engine and **blocks risky shell, file, and network actions** before
they execute.

Two install surfaces — pick one:

| Surface | Lives at | Best for |
|---|---|---|
| **Runtime plugin** (this package) | `.cline/plugins/agentguard/` | TS plugin with typed `beforeTool` / `afterTool` hooks, runs in-process |
| **File hook** (`PreToolUse`) | `.cline/hooks/PreToolUse.js` | Stdin/stdout JSON contract, language-agnostic, also installs alongside this plugin |

Both reuse the same `ClineAdapter` and AgentGuard engine — detection logic
stays in one place.

## Requirements

- Cline ≥ the version that ships the SDK plugin system (`@cline/core`'s
`AgentPlugin` with `hooks.beforeTool`). See
[Cline plugin examples](https://github.com/cline/cline/tree/main/sdk/examples/plugins).
- The AgentGuard engine reachable as the `agentguard` CLI on `PATH`
(`npm i -g @goplus/agentguard`), or installed locally where the plugin can
`import('@goplus/agentguard')`.

## Install

```bash
# Installs the plugin into ~/.cline/plugins/agentguard/ and a file-hook
# script into ~/.cline/hooks/PreToolUse.js (you can keep one or both).
agentguard init --agent cline

# Then enable it inside Cline:
cline plugin install ~/.cline/plugins/agentguard
cline plugin list
```

Or install the runtime plugin directly from GitHub:

```bash
cline plugin install https://github.com/GoPlusSecurity/agentguard/blob/main/plugins/cline/index.ts
```

## What it does

| Cline hook | Behavior |
|-------------------|-----------------------------------------------------------------------|
| `beforeTool` | Evaluates the call; returns `{ skip: true, reason }` to veto a dangerous action, `{ review: true }` to pause for user confirmation. |
| `afterTool` | Audit-only; never blocks. |

Tools evaluated (others pass through untouched):
`run_commands`, `execute_command`, `write_to_file`, `write_file`,
`replace_in_file`, `editor`, `read_files`, `read_file`, `web_fetch`,
`browser_action`, `web_search`.

Cline's `beforeTool` natively supports both `skip` (cancel) and `review` (pause
for user approval), so AgentGuard's `deny` decisions map to `skip` and `ask`
decisions map to `review` — no lossy translation needed.

## Configuration

| Env var | Default | Effect |
|---|---|---|
| `AGENTGUARD_CLINE_FAIL_OPEN` | `0` | `1` allows tool calls when the engine cannot be loaded or errors. Default is fail-closed — both the runtime plugin and the file-hook share this single env var so enforcement is consistent across surfaces. |
| `AGENTGUARD_LEVEL` | `balanced` | Override AgentGuard protection level (`strict` / `balanced` / `permissive`) when no on-disk config is present. |

**Activation:** `agentguard init --agent cline` only writes files. The plugin is
inactive until you run `cline plugin install ~/.cline/plugins/agentguard` (Cline
plugins are opt-in).

**Fail policy:** Out-of-scope tools always pass through without an engine call.
For the security-sensitive tools above, both the runtime plugin and the file
hook default to **fail-closed** (a security gate that quietly turns off isn't a
security gate). Set `AGENTGUARD_CLINE_FAIL_OPEN=1` for local development. This
matches the Hermes plugin's posture and is a single env var across both Cline
surfaces.

## Development

The runtime plugin is a single TypeScript file (`index.ts`) with no runtime
dependencies beyond `@goplus/agentguard` and the Cline SDK. There is no build
step — Cline transpiles plugin TypeScript on install. The shared `ClineAdapter`
that this plugin invokes lives in
[`src/adapters/cline.ts`](../../src/adapters/cline.ts) of the AgentGuard repo
and is exported from `@goplus/agentguard`.
12 changes: 12 additions & 0 deletions plugins/cline/cline.plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "agentguard",
"displayName": "GoPlus AgentGuard",
"version": "1.1.28",
"description": "Security guardrails for Cline — blocks risky shell, file, and network actions before they execute.",
"author": "GoPlusSecurity",
"homepage": "https://github.com/GoPlusSecurity/agentguard",
"license": "MIT",
"entry": "./index.ts",
"capabilities": ["hooks"],
"hooks": ["beforeTool", "afterTool"]
}
167 changes: 167 additions & 0 deletions plugins/cline/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* GoPlus AgentGuard — native Cline runtime plugin.
*
* Implements `@cline/core`'s `AgentPlugin` shape and hooks
* `beforeTool` / `afterTool` into the AgentGuard decision engine
* via the shared `ClineAdapter` + `evaluateHook`.
*
* Install (recommended):
* agentguard init --agent cline
* # Then in Cline:
* cline plugin install ~/.cline/plugins/agentguard
*
* Or directly:
* cline plugin install https://github.com/GoPlusSecurity/agentguard/blob/main/plugins/cline/index.ts
*/

// `@cline/core` provides the AgentPlugin type. We declare a structural fallback
// so this file type-checks inside the AgentGuard repo (which does not depend on
// @cline/core) — at runtime inside Cline, the real type is used.
type BeforeToolResult =
| { skip: true; reason: string }
| { review: true; reason?: string }
| undefined;

interface BeforeToolArgs {
toolCall: { id: string; toolName: string; input: Record<string, unknown> };
input: Record<string, unknown>;
taskId?: string;
workspaceRoots?: string[];
}

interface AfterToolArgs {
toolCall: { id: string; toolName: string; input: Record<string, unknown> };
result?: unknown;
taskId?: string;
}

export interface AgentPlugin {
name: string;
manifest?: { capabilities?: string[] };
hooks?: {
beforeTool?: (args: BeforeToolArgs) => Promise<BeforeToolResult> | BeforeToolResult;
afterTool?: (args: AfterToolArgs) => Promise<void> | void;
};
}

// Lazy-load the engine so the plugin works whether AgentGuard is installed
// alongside Cline (npm i -g @goplus/agentguard) or vendored elsewhere.
let engineCache: {
evaluateHook: (
adapter: unknown,
raw: unknown,
opts: { config: { level?: string }; agentguard: unknown }
) => Promise<{ decision: 'allow' | 'deny' | 'ask'; reason?: string }>;
ClineAdapter: new () => unknown;
loadConfig: () => { level?: string };
createAgentGuard: () => unknown;
} | null = null;

async function loadEngine() {
if (engineCache) return engineCache;
let mod: Record<string, unknown> | undefined;
try {
mod = (await import('@goplus/agentguard')) as Record<string, unknown>;
} catch {
return null;
}
const evaluateHook = mod.evaluateHook as typeof engineCache extends null
? never
: NonNullable<typeof engineCache>['evaluateHook'];
const ClineAdapter = mod.ClineAdapter as typeof engineCache extends null
? never
: NonNullable<typeof engineCache>['ClineAdapter'];
const loadConfig =
(mod.loadConfig as (() => { level?: string }) | undefined) ||
(() => ({ level: process.env.AGENTGUARD_LEVEL || 'balanced' }));
const createAgentGuard =
(mod.createAgentGuard as (() => unknown) | undefined) ||
(mod.default as (() => unknown) | undefined);
if (!evaluateHook || !ClineAdapter || !createAgentGuard) return null;
engineCache = { evaluateHook, ClineAdapter, loadConfig, createAgentGuard };
return engineCache;
}

function envBool(name: string, fallback: boolean): boolean {
const value = process.env[name];
if (value === undefined || value === '') return fallback;
return value === '1' || value.toLowerCase() === 'true';
}

const plugin: AgentPlugin = {
name: '@goplus/agentguard',
manifest: {
capabilities: ['hooks'],
},
hooks: {
async beforeTool({ toolCall, input, taskId, workspaceRoots }): Promise<BeforeToolResult> {
const engine = await loadEngine();
if (!engine) {
// Default: fail-closed — match the file-hook surface and the hermes
// plugin so users get consistent enforcement across integrations.
// Override with AGENTGUARD_CLINE_FAIL_OPEN=1 for local dev.
if (envBool('AGENTGUARD_CLINE_FAIL_OPEN', false)) {
return undefined;
}
return {
skip: true,
reason:
'GoPlus AgentGuard: engine not found (install @goplus/agentguard) — blocking fail-closed. Set AGENTGUARD_CLINE_FAIL_OPEN=1 to allow.',
};
}

const adapter = new engine.ClineAdapter();
const envelope = {
hookName: 'tool_call',
taskId,
workspaceRoots,
tool_call: { id: toolCall.id, name: toolCall.toolName, input },
};

try {
const decision = await engine.evaluateHook(adapter, envelope, {
config: engine.loadConfig(),
agentguard: engine.createAgentGuard(),
});
if (decision.decision === 'deny') {
return { skip: true, reason: decision.reason || 'GoPlus AgentGuard blocked this tool call' };
}
if (decision.decision === 'ask') {
// Cline supports `review` to pause for user confirmation.
return { review: true, reason: decision.reason } as BeforeToolResult;
}
return undefined;
} catch (err) {
if (envBool('AGENTGUARD_CLINE_FAIL_OPEN', false)) {
return undefined;
}
return {
skip: true,
reason: `GoPlus AgentGuard engine error: ${err instanceof Error ? err.message : 'unknown'} — blocking fail-closed. Set AGENTGUARD_CLINE_FAIL_OPEN=1 to allow.`,
};
}
},

async afterTool({ toolCall, taskId }) {
const engine = await loadEngine();
if (!engine) return;
const adapter = new engine.ClineAdapter();
const envelope = {
hookName: 'tool_result',
taskId,
tool_call: { id: toolCall.id, name: toolCall.toolName, input: toolCall.input },
};
try {
await engine.evaluateHook(adapter, envelope, {
config: engine.loadConfig(),
agentguard: engine.createAgentGuard(),
});
} catch {
// Post-tool is audit-only; never affect Cline execution.
}
},
},
};

export { plugin };
export default plugin;
30 changes: 30 additions & 0 deletions plugins/cline/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@goplus/agentguard-cline-plugin",
"version": "1.1.28",
"description": "GoPlus AgentGuard security guardrails for Cline. Hooks every tool call through the AgentGuard decision engine and blocks risky shell, file, and network actions before they execute.",
"type": "module",
"main": "./index.ts",
"exports": {
".": "./index.ts"
},
"keywords": [
"cline",
"cline-plugin",
"agentguard",
"security",
"ai-safety",
"tool-gate"
],
"author": "GoPlusSecurity",
"license": "MIT",
"homepage": "https://github.com/GoPlusSecurity/agentguard",
"repository": {
"type": "git",
"url": "https://github.com/GoPlusSecurity/agentguard.git",
"directory": "plugins/cline"
},
"peerDependencies": {
"@cline/core": "*",
"@goplus/agentguard": ">=1.1.28"
}
}
Loading
Loading