# One-paste hooks: KarmaDue before your agent installs or connects anything

Paste one block into your coding agent's hook config. Before the agent runs `npm install`, `npx`, `pip install`, `uvx` and similar, KarmaDue checks the packages and answers in that agent's own hook format: **block**, **ask your person**, or go ahead. On session start it can also check the project's MCP config. No account. Nothing to install for the Claude Code HTTP variant; everything else uses `karmadue-guard`, one file with no dependencies (Node 18+).

| Client | Before a command | On session start / repo open | Tested |
| --- | --- | --- | --- |
| Claude Code | `PreToolUse` (HTTP or command hook) | `SessionStart` (command hook) | **Live** on the box, Claude Code 2.1.300, scripted model |
| Codex CLI | `PreToolUse` (command hook) | `SessionStart` (command hook) | **Live** on the box, codex-cli 0.162.1, scripted model |
| Cursor | `beforeShellExecution`, `beforeMCPExecution` | `sessionStart` | Not tested in Cursor (no Cursor on the box). The guard's output was checked against Cursor's published format |
| Cline | `PreToolUse` script hook | `TaskStart` script hook | Not tested in Cline (no Cline on the box). The guard's output was checked against Cline's published format |
| Docker MCP Gateway | `--interceptor before:exec:` / `before:http:` | n/a | Not tested in the gateway (no Docker on the box). The live endpoint's replies were checked against the gateway's published format |

"Live" means: the real client binary ran with the snippet below, a scripted stand-in model asked it to run the command, and the client blocked, held or ran it on KarmaDue's answer. "Not tested" snippets follow each client's published hook format, and KarmaDue's replies were checked against that format, but the client itself has not run them.

## What the answers mean

| KarmaDue says | Hook does |
| --- | --- |
| deny, enforce block, or a Squat Watch deny | **block** |
| require_human_approval, unverified_existence (the package does not exist), or a Squat Watch flag | **ask your person** (Codex and Cline cannot ask, so they block with the reason) |
| warn | go ahead, with the reasons added to the agent's context (`--warn ask` / `warn=ask` asks instead) |
| unknown (not assessed) | **ask your person** (since fix103, 2026-11-12; before, fail-open went ahead with a note). Not assessed is not a pass |
| no answer (network, timeout, rate limit), unreadable or malformed hook input, or an item over a limit | **fail-open** (default): go ahead with a loud NOT CHECKED note (Docker's format cannot carry a note on a pass, so there it passes silently). **fail-closed**: block |
| Squat Watch could not be read in time | **fail-open**: at least a warning with a note. **fail-closed**: ask your person |
| a server URL whose path was partly hidden for privacy | **fail-open**: at least a warning with a note. **fail-closed**: ask your person (a verdict for that exact path may not match the hidden path) |
| allow, no_known_issues | go ahead silently |

For a package that does not exist, the reason includes the likely intended package (`reqeusts` → `requests`), chosen by edit distance and registry download counts. It is a suggestion, not a check. See [Squat Watch](https://karmadue.expo.app/docs/squat-watch.md) for names that were registered after agents asked for them.

## What leaves your machine

- **karmadue-guard variants (recommended):** the guard reads the command on your machine and sends only identifiers: `npm:name@version`, `pypi:name==version`, an MCP server URL with user, password, query and fragment removed, a container image name, the server's name from your config, and for a local server only the program's base name (for example `python`). Command lines, paths, env values, headers and tokens stay local.
- **URL redaction:** a server URL is sent as scheme, host and only well-known public path parts (`mcp`, `sse`, `v1`, `api`, `messages`, `stream`, `rpc`, `server(s)`, `gateway`, `tools`, `events` and similar). Every other path segment is replaced with `:redacted`, as is anything after a segment such as `key`, `token`, `secret`, `s` or `session`. The host (including subdomains) is kept, so a token in the host name would leave your machine: do not use such URLs with the hook. The same redaction runs again on KarmaDue's side.
- **Request headers:** the guard also sends a `user-agent` (guard version, OS name and Node version) and an `x-kd-via` label (`karmadue-guard` by default, or the value of `KARMADUE_VIA`) so KarmaDue can count which client made the call. Neither identifies you.
- **Claude Code HTTP variant (zero install):** Claude Code posts its whole hook input (session id, transcript path, cwd and the command line) to KarmaDue. The `if` filters below limit this to commands that start with a package tool, but Claude Code's own docs say the filter is best-effort: when it cannot tell what a command runs, it calls the hook anyway. KarmaDue reads only the command field, keeps only the package identifiers, and does not store the rest. If that is not acceptable, use the command variant.
- **MCP config check:** never send env or headers. The guard drops them before sending. If a request does include `env`, `headers` or token fields, KarmaDue drops them unread and says so.
- Every check creates the usual KarmaDue check record (target, action, time). Each checked npm or PyPI name also passes through Squat Watch's queue (name and time only, no caller identity); names that turn out to exist are kept for 30 days only to avoid re-checking them, and names that do not exist are watched (see [Squat Watch](https://karmadue.expo.app/docs/squat-watch.md)).

## Install the guard (for every variant except Claude Code HTTP)

    curl -fsSL https://karmadue.expo.app/guard/install.sh | KARMADUE_NO_SHIMS=1 sh

Installs `~/.karmadue/guard/karmadue-guard.js` and its two libraries, each checked against a SHA-256 pinned in the installer. `KARMADUE_NO_SHIMS=1` skips the npm/pip PATH shims; leave it out if you also want the shims for people and other agents.

In the snippets, replace `~/.karmadue` with the absolute path (`$HOME/.karmadue` on most systems) if your client does not expand `~`.

## Claude Code

`.claude/settings.json` (project) or `~/.claude/settings.json` (all projects). Claude Code's default hook timeout is 600 s; KarmaDue answers within 30 s.

### Command variant (identifiers only), with session-start MCP config check

```json
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [
        { "type": "command", "command": "node ~/.karmadue/guard/karmadue-guard.js hook claude-code", "timeout": 30 }
      ] }
    ],
    "SessionStart": [
      { "hooks": [
        { "type": "command", "command": "node ~/.karmadue/guard/karmadue-guard.js mcp-config claude-code", "timeout": 30 }
      ] }
    ]
  }
}
```

Fail-closed: add `--fail closed` to the command and `"onFailure": "block"` to the hook (Claude Code 2.1.295+), so a crash or timeout blocks too. Tested live: with an unreachable endpoint, fail-closed blocked and fail-open let the command run.

### HTTP variant (nothing to install)

```json
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [
        { "type": "http", "if": "Bash(npm *)",  "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(npx *)",  "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(pnpm *)", "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(yarn *)", "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(bunx *)", "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(pip *)",  "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(pip3 *)", "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(uv *)",   "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(uvx *)",  "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 },
        { "type": "http", "if": "Bash(pipx *)", "url": "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/claude-code", "timeout": 30 }
      ] }
    ]
  }
}
```

Fail-closed: append `?fail=closed` to each URL and add `"onFailure": "block"`. Claude Code HTTP hooks cannot run on SessionStart, so the MCP config check needs the guard.

## Codex CLI

`~/.codex/hooks.json` or `<repo>/.codex/hooks.json`. Codex asks you to review and trust new hooks (`/hooks` in the CLI) before they run. Codex hooks cannot "ask", so an ask is a deny with the reason. Default timeout 600 s.

```json
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [
        { "type": "command", "command": "node ~/.karmadue/guard/karmadue-guard.js hook codex", "timeout": 30, "statusMessage": "KarmaDue check" }
      ] }
    ],
    "SessionStart": [
      { "matcher": "startup|resume", "hooks": [
        { "type": "command", "command": "node ~/.karmadue/guard/karmadue-guard.js mcp-config codex", "timeout": 30 }
      ] }
    ]
  }
}
```

## Cursor (not tested)

`~/.cursor/hooks.json` or `<repo>/.cursor/hooks.json`. Cursor treats invalid output as a block; the guard always prints valid JSON.

```json
{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [ { "command": "node ~/.karmadue/guard/karmadue-guard.js hook cursor", "timeout": 30 } ],
    "beforeMCPExecution":   [ { "command": "node ~/.karmadue/guard/karmadue-guard.js hook cursor", "timeout": 30 } ],
    "sessionStart":         [ { "command": "node ~/.karmadue/guard/karmadue-guard.js mcp-config cursor", "timeout": 30 } ]
  }
}
```

Fail-closed: `--fail closed` (and Cursor's own `failClosed: true` on the hook, if your version has it).

## Cline (not tested)

Cline runs executable scripts named after the hook from `~/Documents/Cline/Hooks/` (all projects) or `<repo>/.clinerules/hooks/`, with a 30-second limit. Cline cannot ask, so an ask cancels the command with the reason.

`~/Documents/Cline/Hooks/PreToolUse`:

```sh
#!/bin/sh
exec node "$HOME/.karmadue/guard/karmadue-guard.js" hook cline
```

`~/Documents/Cline/Hooks/TaskStart` (checks the workspace's MCP config):

```sh
#!/bin/sh
exec node "$HOME/.karmadue/guard/karmadue-guard.js" mcp-config cline
```

Then `chmod +x` both. The guard stops waiting after 25 s, inside Cline's 30 s.

## Docker MCP Gateway (not tested)

The gateway can run an interceptor before each tool call. A non-empty reply replaces the call; an empty reply lets it through. KarmaDue reads only `command`-like arguments of the call (for shell-style tools).

```sh
docker mcp gateway run --interceptor 'before:exec:node /root/.karmadue/guard/karmadue-guard.js hook docker'
# or, with nothing to install (the tool call's arguments are sent to KarmaDue):
docker mcp gateway run --interceptor 'before:http:https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/docker'
```

## Any other agent

`POST https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/hook/v1/generic` with `{"targets":["npm:left-pad@1.3.0","pypi:reqeusts"]}` or `{"command":"pip install reqeusts"}` returns `{"level":"block|ask|warn|allow","text":"…","targets":[…]}`. Or pipe hook JSON to `karmadue-guard hook generic`.

## MCP config check

`karmadue-guard mcp-config <client> [FILE…]` reads `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.codex/config.toml` (`[mcp_servers.*]`), `claude_desktop_config.json` and `.roo/mcp.json` in the project (or the files you name) and checks each server:

- **Package servers** (`npx`, `pnpm dlx`, `bunx`, `yarn dlx`, `npm exec`, `uvx`, `uv tool run`, `pipx run`): the package is checked. **Unpinned** (`@latest` or no version) is at least a warning, with the version it resolves to today and the pin to add.
- **Remote servers** (`url`): the URL is checked with action `connect` (KarmaDue's remote-MCP assessment applies as it ships). A remote server is at least a warning: it can change on the server side after the check.
- **Local scripts and paths** (`node ./server.js`, `python tools/x.py`, a binary): **ask a person, not checked.** KarmaDue cannot see local code.
- **Container images** (`docker run …`): ask a person; container images are not assessed yet.

The answer goes into the agent's session context (Claude Code, Codex, Cursor, Cline). Or call it directly: `POST …/functions/v1/hook/v1/mcp-config?client=generic` with `{"servers":[…]}` as the guard sends, or `{"config":"<file text>"}` (env and headers are dropped server-side and flagged, but they have already left your machine; prefer the guard). A package started with a custom registry (`--registry`, `--index-url`, `-i…`, `--pip-args`, or a registry env key such as `NPM_CONFIG_REGISTRY`, `UV_INDEX_URL` or `PIP_INDEX_URL`; only the key names are read, never the values) is answered "ask a person" at least: KarmaDue still checks the public package of that name (so a public block stays a block), but what runs may be a different package. In Cursor's `beforeMCPExecution` hook, such a launch command is NOT CHECKED and follows fail-open or fail-closed. Codex `config.toml` env tables are read for key names only, never values.

## Limits

- Per request: 20 packages per command, 20,000 characters per command, 15 MCP servers per config check. Anything over a limit is reported as NOT CHECKED and follows fail-open or fail-closed; nothing is silently dropped.
- Time budget: the guard stops waiting after 25 s for Cursor and Cline (Cline kills hooks at 30 s) and 28 s for the others; Claude Code's own limit is 600 s. The HTTP endpoint takes `budget_ms` (2000–55000; default 25000, Cline 20000, Docker 15000) and answers NOT CHECKED when it runs out.
- Rate limit: per caller IP, one unit per 5 packages or servers checked. When limited, the answer is NOT CHECKED and fail-open or fail-closed applies.
- The MCP config check only adds context to the session. It never blocks the session from starting; the per-command hook does the blocking.
- The `hook` endpoint and the guard's `hook` command read npm and PyPI packages and MCP server URLs from commands. Other package managers (cargo, go, gem, composer, dotnet, mvn), `curl | sh`, `git clone` and `docker pull` are not read from commands by these hooks. The Claude Code script hook (`karmadue-preinstall.js`, [docs](https://karmadue.expo.app/docs/claude-code-hook.md)) does read `cargo install`, `go install` / `go get` and `brew install` and sends them to KarmaDue: since fix103, Rust and Go packages get an exact-version OSV.dev and registry check, and Homebrew is answered "not assessed, ask your person". To check Maven, NuGet, RubyGems, Packagist, crates.io or Go packages directly, see [Other ecosystems](https://karmadue.expo.app/docs/other-ecosystems.md).
- A check is not a safety guarantee. It does not watch what code does when it runs.
- The hook endpoint calls KarmaDue's public check, and all of its calls share one budget on that check, so under heavy load it can answer NOT CHECKED (then fail-open or fail-closed applies). This applies to the guard variants too, since the guard sends through the endpoint.
- Hook formats change. If your client's version differs from the ones tested above, test with `npm install event-stream@3.3.6` (should be blocked) before relying on it.
