# KarmaDue as a PreToolUse hook for goose

Tested: 2026-11-12, 07:19 UTC (2:19 AM ET), with goose 1.59.0 (CLI, Linux x86_64) end to end. No account or API key.

goose hooks can deny a tool call. This plugin puts a `PreToolUse` hook on goose's `shell` tool. The hook finds package installs in the command (the same parser as KarmaDue's Claude Code hook) and asks KarmaDue:

- `block`: goose does not run the command and tells the model `Tool call denied by policy hook ...: BLOCKED by KarmaDue: ...`.
- `require_human_approval`, `unknown`, or no answer: denied too, with "ask your person" (goose hooks have no "ask"). `KARMADUE_WARN=allow` lets these run with a note on stderr.
- `proceed_with_care`, or not an install: allowed.

`"on_failure": "block"` in hooks.json makes goose deny the call if the hook itself fails or times out; goose's default is to let the call through.

## Drop-in file: `hooks.json`

Raw file: https://karmadue.expo.app/recipes/goose/karmadue/hooks/hooks.json

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^shell$",
        "hooks": [
          { "type": "command", "command": "node ${PLUGIN_ROOT}/scripts/karmadue-goose-hook.js", "timeout": 20, "on_failure": "block" }
        ]
      }
    ]
  }
}
```

## Drop-in file: `karmadue-goose-hook.js`

Raw file: https://karmadue.expo.app/recipes/goose/karmadue/scripts/karmadue-goose-hook.js

```js
#!/usr/bin/env node
// KarmaDue pre-install check for goose (PreToolUse hook on the shell tool). No account needed. Node 18+.
// Reads goose's hook payload on stdin, finds package installs in tool_input.command (same parser as
// KarmaDue's Claude Code hook, karmadue-preinstall.js, which must sit next to this file), asks KarmaDue.
//   block                                   -> {"decision":"block"} (goose does not run the command)
//   require_human_approval / unknown / error -> block too, with "ask your person" (goose hooks have no "ask";
//                                              set KARMADUE_WARN=allow to let these run with a stderr note)
//   proceed_with_care / not an install      -> empty stdout, exit 0 (allow)
// stdout carries only the decision; logs go to stderr. Pair with "on_failure": "block" in hooks.json.
"use strict";
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-goose";

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(15000) });
    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 && e.name || e}); not assessed.`, target };
  }
}

(async () => {
  let input = {};
  try { input = JSON.parse(require("fs").readFileSync(0, "utf8") || "{}"); } catch (_) {}
  const command = (input.tool_input && input.tool_input.command) || "";
  const targets = [...new Set(extractInstalls(command).map(targetFor))].slice(0, 6);
  if (!targets.length) process.exit(0);
  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");
  const held = results.filter((d) => d.enforce !== "block" && (d.enforce !== "proceed_with_care" || d.decision === "unknown"));
  const block = (reason) => { process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n"); process.exit(0); };
  if (blocked.length) block(`BLOCKED by KarmaDue: ${blocked.map(line).join(" | ")} Tell your person`);
  if (held.length && (process.env.KARMADUE_WARN || "").toLowerCase() !== "allow")
    block(`NEEDS APPROVAL (KarmaDue): ${held.map(line).join(" | ")} Ask your person; they can run it themselves`);
  for (const d of results) process.stderr.write(`karmadue: ${line(d)}\n`);
  process.exit(0);
})();
```

## Use it

```sh
mkdir -p ~/.agents/plugins/karmadue/hooks ~/.agents/plugins/karmadue/scripts
cd ~/.agents/plugins/karmadue
curl -fsSL -o hooks/hooks.json https://karmadue.expo.app/recipes/goose/karmadue/hooks/hooks.json
curl -fsSL -o scripts/karmadue-goose-hook.js https://karmadue.expo.app/recipes/goose/karmadue/scripts/karmadue-goose-hook.js
curl -fsSL -o scripts/karmadue-preinstall.js https://karmadue.expo.app/hooks/karmadue-preinstall.js
```

Use `<project>/.agents/plugins/karmadue/` instead for one project. Needs Node 18+. You can also add KarmaDue's MCP server as a goose extension (`https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/mcp/safety`, streamable HTTP) so the model can check other targets itself; the hook is what enforces.

## Real output

```text
=== goose run: model asks shell to run: npx -y postmark-mcp@1.0.18
  ▸ shell
    command: npx -y postmark-mcp@1.0.18

Tool call denied by policy hook `karmadue`: 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). [73990cd7-d1c0-4b32-ba8b-3f7f99e29436] Tell your person. Do not retry; this is a policy denial, not a transient failure.done

=== goose run: model asks shell to run: pip install rsquests
  ▸ shell
    command: pip install rsquests

Tool call denied by policy hook `karmadue`: BLOCKED by KarmaDue: pypi:rsquests: Do not install rsquests: malicious-package report MAL-2026-5304 covers its releases, and PyPI has removed the package. [8642500b-7a14-4f1d-bf43-e59435786fc2] Tell your person. Do not retry; this is a policy denial, not a transient failure.done

=== goose run: model asks shell to run: pip install requests==2.31.0
  ▸ shell
    command: pip install requests==2.31.0

Tool call denied by policy hook `karmadue`: NEEDS APPROVAL (KarmaDue): pypi:requests==2.31.0: Caution before you install requests==2.31.0: 3 published advisories affect requests@2.31.0. Fixed in 2.33.0. Ask your person before you do this. [7f6bbc11-2a33-4405-8d35-2c6ef00d71a3] Ask your person; they can run it themselves. Do not retry; this is a policy denial, not a transient failure.done

=== goose run: model asks shell to run: pip install --dry-run requests==2.33.0
  ▸ shell
    command: pip install --dry-run requests==2.33.0

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.
    
    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
```

How it ran: the real goose 1.59.0 CLI (`goose run --no-session`), with the plugin installed under `~/.agents/plugins/karmadue/` and goose's `openai` provider pointed at a local scripted endpoint that asks for one `shell` call per command. So goose itself fired the hook and applied the decision; no model key. The last command was allowed and actually ran; pip then refused only because the test machine's Python is externally managed.

## Notes for this framework

- Only installs named on the command line are checked (not `npm install` from a lockfile, `pip install -r`, git or URL installs). See the parser's limits at https://karmadue.expo.app/docs/any-agent.md.
- Tools from other extensions (`{extension}__{tool}`) are not covered by the `^shell$` matcher.

## 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).
