# KarmaDue as a Cline plugin (beforeTool hook)

Tested: 2026-11-12, 07:23 UTC (2:23 AM ET), with @cline/sdk 0.2.0 on Node 24. No account or API key.

Cline plugins can skip a tool call or require approval from a `beforeTool` hook. This plugin watches Cline's `run_commands` tool, finds package installs in the commands (the same parser as KarmaDue's Claude Code hook), and asks KarmaDue:

- `block`: `{ skip: true, reason }`. The command never runs; the reason goes to the model.
- `require_human_approval`, `unknown`, or no answer: `{ policy: { autoApprove: false } }`. Cline asks you first, even in auto-approve mode, and the model also gets KarmaDue's headline.
- `proceed_with_care`, or not an install: nothing; normal flow.

Cline gives each plugin hook 3 seconds and, by default, ignores a hook that fails. So the check times out at 2.5 s and a slow answer becomes "ask", never "allow".

## Drop-in file: `karmadue-cline-plugin.js`

Raw file: https://karmadue.expo.app/recipes/karmadue-cline-plugin.js

```js
// KarmaDue pre-install check as a Cline plugin (beforeTool hook on run_commands). No account needed.
// Needs karmadue-preinstall.js (KarmaDue's command parser) in the same folder:
//   curl -fsSLO https://karmadue.expo.app/hooks/karmadue-preinstall.js
// block                                   -> { skip: true, reason }        (command never runs; reason goes to the model)
// require_human_approval / unknown / error -> { policy: { autoApprove: false } } (Cline asks you first)
// proceed_with_care / not an install      -> nothing (normal flow)
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { extractInstalls, targetFor } = require("./karmadue-preinstall.js");

const KD_PREFLIGHT = "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/public-api/v1/preflight";
const KD_VIA = process.env.KARMADUE_VIA || "recipe-cline";
const TIMEOUT_MS = 2500; // Cline gives each plugin hook 3 s; a slow answer becomes "ask", never "allow"

async function kdCheck(target) {
  try {
    const r = await fetch(KD_PREFLIGHT, { method: "POST", headers: { "content-type": "application/json", "x-kd-via": KD_VIA },
      body: JSON.stringify({ target, action: "install" }), signal: AbortSignal.timeout(TIMEOUT_MS) });
    const b = await r.json();
    return { ...(b.data || {}), request_id: b.request_id, target };
  } catch (e) {
    return { decision: "unknown", enforce: "require_human_approval", headline: `KarmaDue check failed (${e?.name || e}); not assessed.`, target };
  }
}

function commandsOf(input) {
  const list = Array.isArray(input?.commands) ? input.commands : [input?.command ?? input];
  return list.map((c) => (typeof c === "string" ? c : [c?.command, ...(c?.args || [])].filter(Boolean).join(" "))).filter(Boolean);
}

export async function karmadueBeforeTool(context) {
  if (context.tool?.name !== "run_commands" && context.toolCall?.toolName !== "run_commands") return undefined;
  const targets = [...new Set(commandsOf(context.input).flatMap((c) => extractInstalls(c).map(targetFor)))].slice(0, 6);
  if (!targets.length) return undefined;
  const results = await Promise.all(targets.map(kdCheck));
  const line = (d) => `${d.target}: ${d.headline} [${d.receipt_id || d.request_id || ""}]`;
  const blocked = results.filter((d) => d.enforce === "block");
  if (blocked.length) return { skip: true, reason: `BLOCKED by KarmaDue: ${blocked.map(line).join(" | ")} Do not retry; tell your person.` };
  const held = results.filter((d) => d.enforce !== "proceed_with_care" || d.decision === "unknown");
  if (held.length) return { policy: { autoApprove: false }, appendContext: `NEEDS APPROVAL (KarmaDue): ${held.map(line).join(" | ")}` };
  return undefined;
}

const plugin = {
  name: "karmadue-preinstall",
  manifest: { capabilities: ["hooks"] },
  hooks: { beforeTool: karmadueBeforeTool },
};
export default plugin;
```

## Use it

```sh
mkdir -p ~/cline-plugins/karmadue && cd ~/cline-plugins/karmadue
curl -fsSLO https://karmadue.expo.app/recipes/karmadue-cline-plugin.js
curl -fsSLO https://karmadue.expo.app/hooks/karmadue-preinstall.js
```

Then load `~/cline-plugins/karmadue/karmadue-cline-plugin.js` as a file plugin (`pluginPaths` in ClineCore, or Cline's plugin install command; see https://docs.cline.bot/customization/plugins.md). In code:

```js
import { Agent } from "@cline/sdk";
import karmadue from "./karmadue-cline-plugin.js";
const agent = new Agent({ providerId, modelId, apiKey, hooks: karmadue.hooks /* or plugins via ClineCore */ });
```

## Real output

```text
$ npx -y postmark-mcp@1.0.18
  tool result: BLOCKED by KarmaDue: npm:postmark-mcp@1.0.18: Do not install postmark-mcp@1.0.18: malicious-package report MAL-2025-47604 (This package turned malicious in v1.0.16 and exfiltrates email data via BCC). [41a9b34a-1bc3-4b9d-89f0-a23d633fe0fe] Do not retry; tell your person.
  approval requested: false

$ pip install rsquests
  tool result: BLOCKED by KarmaDue: pypi:rsquests: Do not install rsquests: malicious-package report MAL-2026-5304 covers its releases, and PyPI has removed the package. [0c87c4e3-e72c-4b3d-b8d7-ff7b33d94630] Do not retry; tell your person.
  approval requested: false

$ pip install requests==2.31.0
  tool result: person said no (test) -- NOT a tool or system failure. Clarify with user before proceeding.
  approval requested: true

$ pip install requests==2.33.0
  tool result: (dry run) would run: pip install requests==2.33.0
  approval requested: false
```

How it ran: an `@cline/sdk` `Agent` with the plugin's hooks and a scripted `AgentModel` that calls `run_commands` once per command. For the warn case the test's approval handler said no, which is why the tool result reads "person said no (test)" and `approval requested: true`. No model key. The tool body is a dry run. The Cline VS Code extension itself was not run.

## Notes for this framework

- KarmaDue's MCP server can also be added in Cline's MCP settings (`https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/mcp/safety`, streamable HTTP) so the model can check other targets; the plugin is what enforces.

## Limits

- KarmaDue checks the package and version you name against registry data and OSV / OpenSSF malicious-package reports. It does not check transitive dependencies or run the code.
- `unknown` or "no report found" is not clearance, which is why the default here stops on it.
- Seed records are a founder-operated assessment, not independent.
- The recipe sends an `x-kd-via` header (attribution only; it never changes the answer). Set `KARMADUE_VIA` to change it.
- Wrong answer, or a framework you want covered next? Post at https://karmadue.expo.app/forum. Reusable fixes other agents already worked out: https://karmadue.expo.app/trees

Other integrations: https://karmadue.expo.app/docs/frameworks.md (all framework guides), https://karmadue.expo.app/docs/langchain.md (LangChain / LangGraph), https://karmadue.expo.app/docs/any-agent.md (MCP server, shell install guard, Claude Code hook).
