← Back to Plugins
Channels

Jev Leakguard

yousan By yousan 👁 58 views ▲ 0 votes

Jev × OpenClaw: stop your agent from posting client names, credentials and internal details to the wrong channel. Judged by Jev — or by Kev on your own machine.

GitHub

Install

openclaw plugins install @openclaw/typesafe

README

# Jev × OpenClaw: Leakguard

**Your OpenClaw agent is about to post in a channel. Jev reads the message *and where it's going*, and stops it if a client name, a credential or an internal hostname is about to reach the wrong readers.**

[日本語](README.ja.md) · MIT · works with OpenClaw 2026.9.6+ · judged by your `decisionModel`: hosted **Jev**, or **Kev on your own machine**

<p align="center"><img src="docs/demo.gif" alt="The same status update, naming a client and where its credentials are kept: a confirmation for the private ops channel, blocked for the channel shared with a vendor" width="760"></p>

## Why

An OpenClaw agent of ours was asked for a status update. It posted one — in a channel shared with people outside the team — and the update named the client, the client's domain, and the path on the build server where the credentials for that client were kept. Nothing in it was a password, so no secret scanner would have fired. Every word was true and useful; it was just in the wrong room.

That's what this plugin checks: **this content × this destination**. The same sentence is fine in your private ops channel and not fine in `#shared-with-vendor`.

## Try it in a minute with Jev

You need OpenClaw 2026.9.6+ and a [TypeSafe](https://typesafe.ai) API key.

```sh
# 1. The official TypeSafe plugin provides Jev as OpenClaw's decision model
openclaw plugins install @openclaw/typesafe
openclaw secrets store set TYPESAFE_API_KEY          # paste your key (hidden input)
openclaw config set plugins.entries.typesafe.config.apiKey \
  '{"source":"store","provider":"default","id":"TYPESAFE_API_KEY"}' --json
openclaw config set plugins.entries.typesafe.enabled true --json
openclaw config set agents.defaults.decisionModel '"typesafe/jev-latest"' --json

# 2. This plugin
openclaw plugins install git:github.com/yousan/openclaw-jev-leakguard --accept-capabilities   # confirm the prompt (or add --force)
openclaw config set plugins.entries.jev-leakguard.config '{
  "terms": { "clients": ["Northwind Logistics", "Northwind"] },
  "destinations": [
    { "match": "discord:channel:111111111111111111", "tier": "private" },
    { "match": "slack:#announcements", "tier": "public" }
  ],
  "defaultTier": "shared"
}' --json
openclaw gateway restart

# 3. Ask it directly (the same judgement the hooks make)
openclaw gateway call leakguard.check --params '{
  "dest": "slack:#vendor-shared",
  "text": "Northwind go-live moved to Friday. Their admin creds are in /srv/secrets/nw.env on build-02."
}'
```

```text
jev-leakguard: Blocked — message → slack:#vendor-shared (destination: shared)
  • client (rule term): Nort…****
  • credential (typesafe/kev-latest p=0.626)
  • credential_location (typesafe/kev-latest p=0.708)
  • client (typesafe/kev-latest p=0.56)
```

(That output is from a local Kev-0.8B; with Jev the lines name `typesafe/jev-latest`.)

From now on, every message your agents send goes through the same check.

> [!IMPORTANT]
> **With Jev, the text of each outgoing message is sent to TypeSafe to be judged.** That is how hosted Jev works, and TypeSafe's normal usage charges apply. Your term lists (client names etc.) are **not** sent to a hosted judge; they are matched locally. If the text itself must not leave your machine, use Kev below — nothing else changes.

## Or keep everything on your machine: Kev

The Jev guards we found send the content they check to a hosted API by default — to find out whether something is too sensitive to send out, they send it out. This plugin never calls a judge itself; it asks OpenClaw's `decisionModel`. Point that at a [Kev](https://github.com/jaredpalmer/kev) server (a Jev-compatible model you run yourself) and the check never leaves the machine.

```sh
# On an Apple Silicon Mac (Kev-4B needs ~32 GB; Kev-0.8B runs on any M-series Mac)
git clone https://github.com/jaredpalmer/kev.git && cd kev
uv sync --extra serve
uv run --extra serve python -m kev.serve --run jaredpalmer/kev-4b --port 8009

# In OpenClaw: same typesafe plugin, local endpoint, Kev as the decision model
openclaw config set plugins.entries.typesafe.config '{"baseUrl":"http://127.0.0.1:8009"}' --json
openclaw config set agents.defaults.decisionModel '"typesafe/kev-latest"' --json
```

With a local judge, your term lists are also given to the model as examples, which helps it catch spellings you didn't list.

**No GPU at all?** OpenClaw's `onnx` plugin runs zero-shot classifiers (GLiNER2.5, GLiClass) on the CPU, and this plugin works with them unchanged (`agents.defaults.decisionModel: "onnx/gliner2.5-small-v1"`). We tried — see the numbers below. As of today they flag most ordinary messages too, so treat them as an experiment, not a guard. Looking only at client names, GLiNER2.5-small at a 0.8 threshold caught 6 of 16 and wrongly flagged 7 of the 97 cases without one. The rules (your term lists and key formats) still work with no model at all.

## How it compares

| | Checks the destination | Blocks the outgoing message | Where the content is judged |
|---|---|---|---|
| **This plugin** | ✅ per-channel tiers | ✅ `message_sending`, `reply_payload_sending`, the `message` tool | Your `decisionModel`: Jev, **or Kev / ONNX on your machine** |
| Jev guards for Claude Code and other agents (15+, up to 48★) | – | tool calls / commands | Hosted Jev by default; a few let you point at a local server |
| [herval/openclaw-jev-plugin](https://github.com/herval/openclaw-jev-plugin) | – | – (decides whether to *reply*) | Hosted Jev |
| [trietphan/jev-claw](https://github.com/trietphan/jev-claw) | – | – (routes to a model) | Hosted Jev |
| [jason-allen-oneal/openclaw-plugin-typesafe-ai](https://github.com/jason-allen-oneal/openclaw-plugin-typesafe-ai) | – | tool calls (risk score) | Hosted Jev |
| [larches-technologies/openclaw-jev-router](https://github.com/larches-technologies/openclaw-jev-router) | – | – (tool routing; regex check on *prompts*) | Hosted Jev |

We looked through the OpenClaw × Jev plugins on GitHub (all ★4 or fewer on 2026-09-30) and found none that looks at where a message is going and stops the message itself.

## How it decides

1. **Rules, on your machine.** Known key formats (AWS, GitHub, Slack, OpenAI, Anthropic, Stripe, private keys, JWTs, URLs with passwords…) and your own term lists. A hit that the destination's policy blocks is final; the model isn't asked.
2. **Your decision model.** Five yes/no questions about the text, asked in one batch through `api.runtime.decisions.evaluate`: does it contain a *credential*, say *where credentials are kept*, name a *client*, reveal *internal* infrastructure, or disclose *confidential* business information? Long messages are split and every part is judged.
3. **The destination's policy.** Each destination has a tier; each tier says what to do per category:

| Found | public | shared (default) | private |
|---|---|---|---|
| credential | block | block | ask |
| where credentials are kept | block | block | allow |
| client name | block | ask | allow |
| internal hostnames, IPs, channel IDs | block | ask | allow |
| confidential business info | block | ask | allow |

"Ask" becomes an approval prompt when the agent uses the `message` tool (`before_tool_call` → `requireApproval`). A reply already on its way to a channel has no one to ask, so there "ask" holds the message back.

**Failing closed.** OpenClaw's delivery hooks fail *open*: a handler that throws or takes longer than 15 s is skipped and the message goes out. So this plugin never throws, stops waiting after `deadlineMs` (12 s), and holds the message back itself. If the decision model is unavailable, the default is also to hold it back (`onError`).

## Configuration

Everything lives under `plugins.entries.jev-leakguard.config`:

```json5
{
  terms: {
    clients: ["Northwind Logistics", "ミナト精機"],   // matched locally; ASCII on word boundaries
    internal: ["intra.example-corp.net", "ops-db-01"],
    confidential: ["Project Falcon"],
    patterns: [{ name: "invoice", category: "confidential", regex: "INV-\\d{6}" }],
  },
  destinations: [                                   // "<channel>:<target>", * wildcards, first match wins
    { match: "discord:channel:111111111111111111", tier: "private" },
    { match: "slack:#announcements", tier: "public" },
    { match: "slack:*", tier: "shared" },
  ],
  defaultTier: "shared",
  policy: { shared: { client: "block" } },          // override any cell of the table above
  threshold: 0.5,                                   // or per category: thresholds: { client: 0.7 }
  onError: "block",                                 // block | ask | allow, when the judge can't answer
  deadlineMs: 12000,
}
```

Each block is logged with the destination id it used (`message → slack:#vendor-shared`), which is the easiest way to find the id to put in `destinations`.

## Evaluation

A synthetic test set: 56 leaks and 57 ordinary messages in English and Japanese — commit messages, issues, chat updates, MCP-style tool input. Every company, host and key is made up; keys are generated at run time so the repository holds nothing in a real key format. 30 of the leaks can't be caught by a regex or a term list (unlisted client names, paraphrased credential locations, prices quoted to a customer…). Each judge was run through OpenClaw's `decisionModel`, exactly as the plugin calls it.

<!-- results:start -->
| Judge (`decisionModel`) | Text goes to | Missed leaks | …that a regex can't see | False alarms | Missed at ≤10% false alarms | Latency p50 | p95 |
|---|---|---|---|---|---|---|---|
| Rules only (regex + your term lists) | nowhere | 54% (30/56) | 100% (30/30) | 0% (0/57) | – | <1 ms | <1 ms |
| Rules + Jev (`typesafe/jev-latest`) | **TypeSafe API** | 0% (0/56) | 0% (0/30) | 7% (4/57) | 0% @ 0.41 | 223 ms | 293 ms (hosted) |
| Rules + Kev-0.8B (`typesafe/kev-latest`) | this machine | 21% (12/56) | 40% (12/30) | 11% (6/57) | 27% @ 0.59 | 4.8 s | 8.4 s (x86 CPU, no GPU) |
| Rules + GLiNER2.5 small (`onnx/gliner2.5

... (truncated)
channels

Comments

Sign in to leave a comment

Loading comments...