Skip to content

feat(cline): add native Cline plugin#110

Open
theyavuzarslan wants to merge 3 commits into
GoPlusSecurity:mainfrom
theyavuzarslan:feat/cline-plugin
Open

feat(cline): add native Cline plugin#110
theyavuzarslan wants to merge 3 commits into
GoPlusSecurity:mainfrom
theyavuzarslan:feat/cline-plugin

Conversation

@theyavuzarslan

Copy link
Copy Markdown
Contributor

Summary

Adds a Cline integration parallel to the existing Hermes and Claude Code adapters, so AgentGuard can sit in front of Cline's tool-execution loop and block risky shell, file, and network actions before they run.

Cline ships both a typed runtime plugin SDK (@cline/core's AgentPlugin with hooks.beforeTool / afterTool) and a stdin/stdout file-hook system (.cline/hooks/PreToolUse.*, PostToolUse.*). This PR wires AgentGuard into both, reusing the existing HookAdapter / evaluateHook engine — detection logic stays in one place.

What changed

  • src/adapters/cline.tsClineAdapter mapping Cline tool names (run_commands, execute_command, write_to_file/write_file/replace_in_file/editor, read_files/read_file, web_fetch, browser_action, web_search) to the shared adapter contract.
  • plugins/cline/ — Native Cline runtime plugin (@cline/core AgentPlugin shape) with beforeTool/afterTool. deny{skip, reason}, ask{review, reason} — Cline supports both natively, no lossy translation.
  • skills/agentguard/scripts/cline-hook.js — File-hook bridge handling Cline's tool_call / tool_result JSON protocol; delegates to the unified protectAction API with agentHost: 'cline' and falls back to ClineAdapter + evaluateHook on older engine builds.
  • agentguard init --agent cline — Copies the skill, drops executable PreToolUse.js / PostToolUse.js shims into .cline/hooks/, and stages the runtime plugin in .cline/plugins/agentguard/.
  • Wired 'cline' through AgentInstaller, RuntimeAgentHost, AgentGuardAgentHost, SUPPORTED_AGENT_INSTALLERS, and AUTO_AGENT_DETECTION. Narrowed resolveCronAgentHost to the cron-capable subset so the new host doesn't leak into cron backends that don't target it.

Install / use

npm i -g @goplus/agentguard       # or local install
agentguard init --agent cline     # writes .cline/{skills,hooks,plugins}/...

# Then inside Cline:
cline plugin install ~/.cline/plugins/agentguard

Or use only the file hooks — agentguard init --agent cline writes them by default.

Fail policy

Mirrors Hermes:

  • Out-of-scope tools always pass through without an engine call.
  • File hook fails closed by default (AGENTGUARD_CLINE_FAIL_OPEN=1 to invert).
  • Runtime plugin fails open by default (AGENTGUARD_CLINE_FAIL_CLOSED=1 to invert) since users may not always have the engine installed alongside Cline.

Test plan

  • npm run build — clean
  • npm test — all 415 existing tests pass, no regressions
  • Smoke: run_commands rm -rf /{"cancel":true,"errorMessage":"..."}
  • Smoke: run_commands echo hello{}
  • Smoke: write_to_file .env{"review":true,"context":"..."}
  • agentguard init --agent cline writes ~/.cline/skills/agentguard, executable PreToolUse.js/PostToolUse.js shims, and ~/.cline/plugins/agentguard/{index.ts,package.json,README.md,cline.plugin.json}
  • (out of PR scope for first pass, matching hermes' initial PR) Dedicated plugins/cline/tests/ — happy to follow up if you want me to add them in this PR.

References:

🤖 Generated with Claude Code

Adds a Cline integration parallel to the existing Hermes and Claude Code
adapters, so AgentGuard can sit in front of Cline's tool-execution loop and
block risky shell, file, and network actions before they run.

Surfaces:
- ClineAdapter (src/adapters/cline.ts) maps Cline tool names (run_commands,
  execute_command, write_to_file/write_file/replace_in_file/editor,
  read_files/read_file, web_fetch, browser_action, web_search) to the shared
  HookAdapter contract used by the engine.
- Runtime plugin (plugins/cline/) implements @cline/core's AgentPlugin shape
  with hooks.beforeTool/afterTool — install via `cline plugin install
  ~/.cline/plugins/agentguard`.
- File-hook bridge (skills/agentguard/scripts/cline-hook.js) handles Cline's
  stdin/stdout PreToolUse/PostToolUse protocol, mapping deny -> cancel and
  ask -> review (Cline supports both natively, no lossy translation).
- `agentguard init --agent cline` copies the skill, drops PreToolUse.js /
  PostToolUse.js shims into .cline/hooks/ (chmod +x), and stages the runtime
  plugin in .cline/plugins/agentguard/.

Wiring:
- 'cline' added to AgentInstaller, RuntimeAgentHost, AgentGuardAgentHost,
  SUPPORTED_AGENT_INSTALLERS, and AUTO_AGENT_DETECTION.
- resolveCronAgentHost narrowed to the cron-capable subset so the new host
  doesn't leak into cron backends that don't target it.

Fail policy mirrors Hermes: out-of-scope tools always pass through; for
in-scope tools the file-hook fails closed by default (override:
AGENTGUARD_CLINE_FAIL_OPEN=1) and the runtime plugin fails open by default
(override: AGENTGUARD_CLINE_FAIL_CLOSED=1) since users may not always have
the engine installed alongside Cline.

Existing 415 tests pass. End-to-end smoke:
- run_commands rm -rf /     -> {"cancel":true,"errorMessage":"..."}
- run_commands echo hello   -> {}
- write_to_file .env        -> {"review":true,"context":"..."}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

AgentGuard PR Review

I found a few concrete regressions and security gaps in the Cline integration.

  1. severity: highsrc/cli.ts / src/installers.ts / src/config.ts (cline support wiring)

    • What can go wrong: cline is added as a supported agentHost/installer/config value, but there is no corresponding update to any cron/backend logic beyond filtering it out in resolveCronAgentHost. If other code paths assume every configured agentHost has a working installer/template/backend, cline configs can become partially supported and break automation or leave users with a config that cannot be acted on consistently.
    • Fix: Add explicit validation wherever agentHost is consumed so cline is only accepted in the Cline-specific init/install flow, and ensure any non-Cline operations reject or ignore it with a clear error. Add tests for init/autodetect and any config serialization/deserialization paths.
  2. severity: highskills/agentguard/scripts/cline-hook.js (loadEngine() / agentguardPath fallback)

    • What can go wrong: The hook tries to import a relative dist/index.js using import.meta.url.replace('file://', '') and join(...), then falls back to @goplus/agentguard. This path resolution is fragile and can silently fail depending on how the hook is copied/installed. On failure, the script may fail open unless AGENTGUARD_CLINE_FAIL_CLOSED is set, which is unsafe for a security gate.
    • Fix: Resolve the bundled engine path relative to the installed hook location with new URL() or fileURLToPath(import.meta.url) and make the fail-closed behavior the default for security-sensitive tools. Only allow fail-open behind an explicit opt-in and log a hard error when the engine cannot be loaded.
  3. severity: mediumskills/agentguard/scripts/cline-hook.js (shouldFailClosed, FAIL_OPEN env handling)

    • What can go wrong: The environment variables are inconsistent across the runtime plugin and file-hook bridge: the plugin uses AGENTGUARD_CLINE_FAIL_CLOSED, while the file-hook uses AGENTGUARD_CLINE_FAIL_OPEN. This makes the effective security posture easy to misconfigure and can accidentally disable blocking when the engine is unavailable.
    • Fix: Use one env var name and one default policy in both surfaces. Prefer fail-closed by default for pre-tool hooks, and add tests proving the same env setting produces the same behavior in both plugin and file-hook modes.
  4. severity: mediumsrc/installers.ts (installCline() copying plugin files)

    • What can go wrong: The installer copies the TypeScript plugin source directly into ~/.cline/plugins/agentguard/, but the plugin package declares main/exports as ./index.ts without any build artifact. If Cline’s plugin loader does not transpile TS in the user’s environment or expects a compiled entrypoint, plugin install will succeed but the plugin will not load, causing silent loss of protection.
    • Fix: Ship a compiled JavaScript entrypoint in the plugin package or verify Cline’s loader supports raw TS. Add an installation smoke test that loads the installed plugin exactly as Cline does.
  5. severity: mediumplugins/cline/index.ts (afterTool path and fallback behavior)

    • What can go wrong: afterTool always tries to load the engine and swallows all errors. If the engine is missing or misconfigured, audit events disappear silently, making it impossible to detect failed protection coverage. More importantly, the same engine-loading failure path is shared with beforeTool, where fail-open is possible.
    • Fix: Emit a warning or telemetry event on engine load failure so operators can detect that the plugin is inert. For beforeTool, keep blocking by default when the engine cannot load.

theyavuzarslan and others added 2 commits June 19, 2026 01:16
Adds 32 tests bringing the suite from 415 to 447, all passing.

src/tests/adapter.test.ts — ClineAdapter unit tests:
- parseInput covers both the file-hook shape (tool_call.{name,input}) and
  the runtime-plugin shape (toolCall.toolName), tool_result -> 'post',
  workspaceRoots[0] -> cwd, preToolUse.parameters fallback, and empty input
- mapToolToActionType covers every entry in TOOL_ACTION_MAP plus null for
  out-of-scope tools (use_mcp_tool, ask_followup_question, '')
- buildEnvelope covers run_commands string + commands[] array (joined),
  write_to_file, read_files with files: [{path}], web_fetch, web_search,
  and null return for unmapped tools
- inferInitiatingSkill: null by default, honors initiating_skill metadata

src/tests/smoke.test.ts — cline-hook.js subprocess E2E:
- allow echo hello
- block rm -rf / via {cancel, errorMessage}
- map AgentGuard 'ask' to Cline {review, context} for .env writes
- accept runtime-plugin toolCall shape
- flatten run_commands.commands array before evaluating (catches the
  joined `echo ok && rm -rf /` case)
- post-tool events audit-only -> {}
- out-of-scope tools (use_mcp_tool) passthrough -> {}
- missing required field (run_commands without command) -> block
- AGENTGUARD_TEST_FORCE_ENGINE_LOAD_FAILURE=1 fails closed by default
- AGENTGUARD_CLINE_FAIL_OPEN=1 inverts to allow on engine failure
- invalid stdin exits promptly with cancel decision

Bug fix surfaced by the commands-array test: cline-hook.js's
normalizeForRuntime now flattens `tool_input.commands: string[]` into a
single `command` field before handing off to protectAction. The runtime
engine's input picker only looks at `command`/`cmd`, so without the
flatten the array passed through unevaluated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five issues raised in review; this commit fixes four and verifies the fifth
was a false positive.

1. HIGH — installer no longer writes a spawnSync shim.
   PreToolUse.js and PostToolUse.js are now cline-hook.js copied verbatim
   (same script, branches internally on hookName). One fewer process, no
   stdin-inheritance question. Installer test asserts byte-identity with
   the bundled source and that the file does not contain `spawnSync`.

2. HIGH — packaging was already correct.
   `plugins` is in package.json "files", so plugins/cline ships with the
   npm package. Added a comment in installers.ts noting this so the path
   contract is explicit.

3. MEDIUM — robust path resolution in cline-hook.js.
   Replaced `import.meta.url.replace('file://', '')` (broken on Windows,
   wrong join semantics) with `fileURLToPath() + dirname()`, and only
   attempt the bundled engine when the resolved path actually exists.
   Falls through to the `@goplus/agentguard` bare specifier otherwise.

4. MEDIUM — multi-file read_files prefers the worst-case path.
   When read_files lists multiple files, the adapter now scans them with
   isSensitivePath and picks any sensitive target as the envelope's
   primary path (with the full list in `paths` and the flagged target in
   `sensitive_path`). A two-file [benign, .env] read now blocks instead
   of slipping through on the first benign entry. Two adapter tests lock
   the contract in (sensitive-wins + paths preserved for benign lists).

5. MEDIUM — both Cline surfaces now default fail-closed under one env var.
   Runtime plugin previously defaulted fail-open via
   AGENTGUARD_CLINE_FAIL_CLOSED. Now both the file hook and the runtime
   plugin default to fail-closed and read AGENTGUARD_CLINE_FAIL_OPEN=1 to
   invert. A security gate that quietly turns off isn't a security gate;
   this matches the Hermes plugin's posture and removes the cross-surface
   divergence the reviewer flagged.

Tests: 451/451 pass (was 447; +4 new — multi-file sensitive/benign, hook
copied-not-shimmed, hook executable bit).
End-to-end verified: `agentguard init --agent cline --force` against a
fresh temp dir installs PreToolUse.js byte-identical to the source, and
invoking it as Cline does correctly fails closed when the engine is
unreachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@theyavuzarslan

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All five items addressed in 750bc9d:

# Severity Status Fix
1 high — spawnSync shim could lose stdin ✅ Fixed Dropped the shim. PreToolUse.js/PostToolUse.js are now cline-hook.js copied verbatim — single process, one source of truth, no stdin-inheritance question. Installer test asserts byte-identity with the source and that the installed file contains no spawnSync.
2 high — plugins/cline not in published package ✅ Already correct plugins is already listed in package.json "files", so plugins/cline ships with the npm package. Added an inline comment in installers.ts next to the resolve(__dirname, '..', 'plugins', 'cline') lookup so this contract can't silently regress.
3 medium — brittle path resolution in cline-hook.js ✅ Fixed Replaced import.meta.url.replace('file://', '') with fileURLToPath() + dirname(). The bundled-engine path is only tried when it actually exists; otherwise we fall through to the documented @goplus/agentguard bare-specifier path (works for the recommended npm i -g @goplus/agentguard install).
4 medium — multi-file read_files under-evaluated ✅ Fixed Adapter now collects every path from path / file_path / files[] / file_paths[] / paths[], runs isSensitivePath over the set, and elevates any sensitive target as the envelope's primary path (with the full list in paths and the flagged target in sensitive_path). A two-file [benign, .env] read now blocks instead of passing on the first benign entry. Two new adapter tests lock the contract in.
5 medium — inconsistent fail policy across surfaces ✅ Fixed Both surfaces now default fail-closed under a single env var: AGENTGUARD_CLINE_FAIL_OPEN=1 to invert. Removed AGENTGUARD_CLINE_FAIL_CLOSED. Matches the Hermes plugin posture and removes the cross-surface divergence. README updated.

Tests: 451/451 pass (was 447 before the review fixes; +4 new — multi-file sensitive picking, multi-file benign passthrough, hook-copied-not-shimmed byte-identity, hook executable bit on POSIX).

End-to-end smoke against a fresh temp install dir:

  • PreToolUse.js is a byte-identical copy of skills/agentguard/scripts/cline-hook.js
  • Invoking it directly (as Cline does) correctly fails closed when the engine is unreachable
  • With the engine reachable: rm -rf /{cancel}, .env write → {review}, echo hello{}

Happy to split into smaller commits or rebase if that's easier to review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant