The AI World · Article 9

How to use TypeSafe Jev with Claude: a step-by-step guide

Five ways to make a fast judgment model and a thinking model work as one team — the Claude Code skill, an MCP tool, hooks that guard what runs, a router in front of Claude and a verifier behind it — with real runs, real code and the thresholds that hold it together.

By 20 min read

How to read this

New to all this? Read the green sections: they explain the idea with everyday examples and get you set up. Using Claude Code every day? The amber sections add Jev to it in minutes. Shipping an app on the Claude API? The red sections are the architecture, the code and the numbers.

BeginnerIntermediateAdvanced

Claude is brilliant at the slow, careful work: writing, reasoning, coding, planning. But a surprising amount of what happens around Claude is tiny decisions. Is this request something a database can answer? Is this shell command about to delete a folder? Did this draft just promise a refund we don’t offer? Asking Claude each of those is like asking a senior engineer to sort the post.

Jev, from a company called TypeSafe, is built for exactly those tiny decisions. You give it a situation and a few typed questions; it gives back a choice, a probability or a position on a scale[1] — in a fraction of a second, for a fraction of a cent, with an honest measure of how sure it is. Our last TypeSafe article explained what Jev is and why it works. This one is the practical sequel: five concrete ways to make Jev and Claude work as one team, each with steps you can follow today.

01Beginner

Two models, one team

Think of a busy clinic. At the front desk sits a receptionist who glances at each visitor and makes a quick call: appointment, prescription pickup, emergency. Behind the doors sit the doctors, who take their time, examine, explain and write. The clinic works because each does what they are best at. Nobody wants the surgeon on the front desk, and nobody wants the receptionist to operate.

Jev

The receptionist. Reads the situation and makes one quick, typed call.

Returns: a choice, a probability, a level — with calibrated confidence

Feels like: ~0.3–0.5 s in our runs · pays for input only

routecheckscoregateverify

Claude

The specialist. Thinks at length, writes, codes and uses tools.

Returns: prose, code, plans, tool calls

Feels like: seconds to minutes · pays for input and output

writereasoncodeexplainact
Fig. 1 — Jev is the receptionist; Claude is the specialist. Speeds are from our own runs, network included.

That is the whole idea of this guide. Jev makes the quick calls. Claude does the thinking and the writing. Plain code sits between them and owns the rules — which thresholds to trust, what happens when a model is unsure, what happens when one is unavailable.

02Beginner

Five ways to connect them

There are five places Jev can sit relative to Claude. The first three live inside Claude Code, Anthropic’s coding agent in the terminal. The last two live inside your own app built on the Claude API.

1

The skill

Claude Code writes Jev code

Install TypeSafe’s plugin and Claude Code learns the API, the primitives and the patterns before it writes a line.

For: Builders · Intermediate

2

An MCP tool

Claude calls Jev while it works

A tiny server gives Claude a jev_evaluate tool, so it can ask quick typed questions mid-task.

For: Power users · Intermediate

3

Hooks & commands

Jev watches Claude Code

Before a risky shell command runs, or before a prompt is sent, Jev scores it and your rules decide.

For: Everyone using Claude Code · Intermediate

4

A router

Jev in front of Claude, in your app

Jev sorts every request first. Easy ones never reach Claude; hard ones reach it with notes.

For: App builders · Advanced

5

A verifier

Jev after Claude, in your app

Claude drafts, Jev checks the draft against your policy and facts, code decides: send, redo or escalate.

For: App builders · Advanced

Fig. 2 — The five ways, from easiest to most powerful. They combine: many real set-ups use two or three.

Not sure where to start? Answer five questions:

  1. Q1Do you want Claude Code to build something that uses Jev?
    Way 1 — install the skill
  2. Q2Do you want Claude to ask Jev quick questions during a task?
    Way 2 — add an MCP tool
  3. Q3Do you want a safety net on what Claude Code runs or receives?
    Way 3 — a hook or a slash command
  4. Q4Are you shipping an app where every request hits Claude today?
    Way 4 — put a Jev router in front
  5. Q5Does Claude write things that must follow rules or facts?
    Way 5 — verify its drafts with Jev
  6. No to all? Start with the Playground and come back — most people end up using two or three of these together.
Fig. 3 — A quick decision guide. Each “yes” points to one section of this article.
03Beginner

Step 0: a key, kept safe

Every way below needs one thing: a TypeSafe API key. Getting one takes about five minutes.

  1. Sign up

    console.typesafe.ai

  2. Playground

    paste text, ask questions

  3. Create a key

    console.typesafe.ai/keys

  4. Store it safely

    Keychain or a secret store

  5. Check it

    without printing it

Fig. 4 — Setup, once. Try the Playground before any code — it is the cheapest place to learn how questions behave.
  1. Sign up at console.typesafe.ai.
  2. Play first. Open the Playground, paste a real message from your work as the state, and add one question, such as a Noul: “Does this message express urgency?” Add a Choice and a Score. Watch how the numbers move when you change the wording.
  3. Create a key on the Keys page and store it where it can’t leak. On a Mac, the Keychain is ideal:
Terminal — store the key (macOS)
# macOS: keep the key in the Keychain. -w with no value makes it ask,
# so the key never lands in your shell history.
security add-generic-password -a "$USER" -s typesafe-api -w

# Make it available to the terminals that start Claude Code
echo 'export TYPESAFE_API_KEY="$(security find-generic-password -s typesafe-api -w)"' >> ~/.zshrc
source ~/.zshrc

# Check it works without printing it
[ -n "$TYPESAFE_API_KEY" ] && echo "key loaded (${#TYPESAFE_API_KEY} characters)"

A quick word on cost, because it shapes every design choice below. Jev charges only for input tokens — $0.042 per million at the time of writing — and output is free.[3] Our router call in Way 4 used 522 input tokens: about $0.000022, or roughly $22 for a million customer messages.

cost = input tokens × $0.042 / 106

522 tokens × $0.042 per million tokens ≈ $0.0000219 per call

04Intermediate

Way 1: the TypeSafe skill in Claude Code

The fastest win. A skill is a briefing that Claude Code loads when it’s relevant: instructions, rules and pointers to documentation. TypeSafe publishes one as a Claude Code plugin.[4] With it installed, Claude Code knows the three question types, the design patterns and the pitfalls — and, importantly, it reads TypeSafe’s live documentation before writing code, rather than relying on memory.

TypeSafe documentation page titled Agent skill, showing the Claude Code tab with two commands: claude plugin marketplace add typesafe-ai/skills, and claude plugin install typesafe@typesafe-ai.
Fig. 5 — Real screenshot of TypeSafe’s “Agent skill” documentation page, captured on 28 September 2026. The two commands are all it takes.

Step 1 — install. In any terminal:

Terminal — install and update the plugin
claude plugin marketplace add typesafe-ai/skills
claude plugin install typesafe@typesafe-ai

# later, to update
claude plugin marketplace update typesafe-ai
claude plugin update typesafe@typesafe-ai

Step 2 — check it’s there.

Terminal — real run

● real run · 28 Sep 2026

$ claude plugin list
  …
  ❯ typesafe@typesafe-ai
    Version: 0.5.7
    Scope: user
    Status: ✔ enabled

Step 3 — use it. Start Claude Code in your project and either type /typesafe:typesafe-ai followed by your request, or simply say “use the TypeSafe skill”. Good first prompts:

  • “Using the TypeSafe skill, explore this project and find fragile parsing or if/else logic that an intelligent judgment could replace.”
  • “Using the TypeSafe skill, run a few cheap experiments with the key in TYPESAFE_API_KEY and propose changes based on the most promising results.”
  • “Using the TypeSafe skill, check whether any TypeSafe cookbook matches a problem in my code.”
How the TypeSafe skill works inside Claude Code. You ask Claude Code to use the skill; it loads TypeSafe’s design rules, reads the live documentation, proposes a plan with the questions and thresholds for you to review, and only then writes code.You“use the TypeSafeskill to…”Claude Codeloads the skill’sdesign rulesLive docsreads the API, SDKand a cookbookPlanquestions andthresholds to reviewCodeone call, typedanswers, fails openyou edit the questions, it tests againthe skill is free — only the test calls it makes cost Jev tokens
Fig. 6 — What happens inside Claude Code when the skill is active. The loop at the bottom is where most of the quality comes from.
05Intermediate

Way 2: give Claude a Jev tool with MCP

The skill helps Claude write code that calls Jev. Sometimes you want Claude to call Jev itself, right in the middle of a task: “which of these 60 log lines are relevant?”, “which of these five commit messages is clearest?”. For that, give Claude a tool.

MCP — the Model Context Protocol — is the standard way to plug tools into Claude Code and Claude Desktop.[5] (Our agentic AI article explains it.) An MCP server is just a small program that announces its tools and runs them when asked. TypeSafe doesn’t ship an official one today, so we’ll build one — about 80 lines, no dependencies.

Sequence of an MCP tool call. Claude asks the local jev MCP server which tools it has and sees jev_evaluate. When Claude calls the tool with state and questions, the server adds the API key from the Keychain and posts to TypeSafe’s API. Answers come back in about 300 milliseconds and are returned to Claude as JSON. The key never enters Claude’s context.Claude(Code or Desktop)jev MCP servernode, on your MacTypeSafe API/v1/systemonetools/list → sees jev_evaluatetools/call {state, questions}POST + Bearer key (from Keychain)answers + usage · ~300 msJSON result + latency_ms
Fig. 7 — What happens when Claude uses the tool. The API key is read by the server and never enters Claude’s context.

Step 1 — save the server. This is a condensed version of the server this article was researched with. It speaks MCP over standard input and output, one JSON message per line:

~/jev-mcp/server.mjs
#!/usr/bin/env node
// ~/jev-mcp/server.mjs — gives Claude one tool, jev_evaluate. Zero dependencies.
import { execFileSync } from "node:child_process";
import { createInterface } from "node:readline";

const API = "https://api.typesafe.ai/v1/systemone";

function apiKey() {
  if (process.env.TYPESAFE_API_KEY) return process.env.TYPESAFE_API_KEY.trim();
  try {
    // macOS Keychain, so the key never sits in a config file
    return execFileSync("/usr/bin/security", ["find-generic-password", "-s", "typesafe-api", "-w"], {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    }).trim();
  } catch {
    return null;
  }
}

const TOOL = {
  name: "jev_evaluate",
  description:
    "Ask TypeSafe's Jev typed questions about some state. noul → probability of yes; " +
    "choice → one option + probabilities + confidence; score → position on 2–10 ordered levels. " +
    "Batch every independent question about the same state into ONE call.",
  inputSchema: {
    type: "object",
    properties: {
      state: { description: "Text, or an object of named fields, to judge." },
      questions: { type: "object", description: "Map of id → {type, instructions, criteria?}." },
      model: { type: "string", description: "Default: jev-latest" },
    },
    required: ["state", "questions"],
  },
};

const fail = (text) => ({ content: [{ type: "text", text }], isError: true });

async function evaluate({ state, questions, model = "jev-latest" }) {
  const key = apiKey();
  if (!key) return fail("No TypeSafe key: set TYPESAFE_API_KEY or add it to the Keychain.");
  const started = Date.now();
  try {
    const res = await fetch(API, {
      method: "POST",
      headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
      body: JSON.stringify({ state, questions, model }),
      signal: AbortSignal.timeout(20_000),
    });
    const text = await res.text();
    if (!res.ok) return fail(`TypeSafe HTTP ${res.status}: ${text.slice(0, 500)}`);
    const body = { ...JSON.parse(text), latency_ms: Date.now() - started };
    return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
  } catch (err) {
    return fail(`TypeSafe request failed: ${err.message}`);
  }
}

// MCP over stdio: one JSON-RPC message per line in, one per line out.
const send = (msg) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");

createInterface({ input: process.stdin }).on("line", async (line) => {
  if (!line.trim()) return;
  const { id, method, params } = JSON.parse(line);
  if (method === "initialize") {
    return send({
      id,
      result: {
        protocolVersion: params?.protocolVersion ?? "2025-06-18",
        capabilities: { tools: {} },
        serverInfo: { name: "jev", version: "1.0.0" },
      },
    });
  }
  if (method === "tools/list") return send({ id, result: { tools: [TOOL] } });
  if (method === "tools/call") return send({ id, result: await evaluate(params?.arguments ?? {}) });
  if (method === "ping") return send({ id, result: {} });
  if (id !== undefined) send({ id, error: { code: -32601, message: `Method not found: ${method}` } });
});

Step 2 — register it with Claude Code, once, for every project:

Terminal — register and check
mkdir -p ~/jev-mcp            # then save server.mjs there
claude mcp add --scope user jev -- node ~/jev-mcp/server.mjs
claude mcp get jev
Terminal — real run (home path shortened)

● real run · 28 Sep 2026

$ claude mcp get jev
jev:
  Scope: User config (available in all your projects)
  Status: ✔ Connected
  Type: stdio
  Command: /opt/homebrew/bin/node
  Args: ~/jev-mcp/server.mjs
  Environment:

Using Claude Desktop instead? Add the server to claude_desktop_config.json (in Settings → Developer → Edit Config), then restart the app. Use absolute paths: desktop apps don’t read your shell profile, which is also why the server looks in the Keychain rather than relying on an environment variable.

claude_desktop_config.json
{
  "mcpServers": {
    "jev": {
      "command": "/opt/homebrew/bin/node",
      "args": ["/Users/you/jev-mcp/server.mjs"]
    }
  }
}

Step 3 — watch it work. Here is the raw conversation between a client and the server — the same messages Claude Code sends — including a real answer from Jev:

stdio — real run (long lines wrapped)

● real run · 28 Sep 2026

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"jev","version":"1.0.0"}}}
→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"jev_evaluate", … }]}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"jev_evaluate","arguments":{
    "state":"Can you please send me the invoice again? I cannot find it.",
    "questions":{"billing":{"type":"noul","instructions":"Is this message about billing or invoices?"}}}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{ \"model\": \"jev-1.13.0\",
    \"answers\": { \"billing\": { \"type\": \"noul\", \"noul\": 0.99 } },
    \"usage\": { \"input_tokens\": 288, \"output_tokens\": 20 }, \"latency_ms\": 865 }"}]}}

In practice you just talk to Claude: “Use jev_evaluate to check which of these 40 reviews mention a crash — one Noul per review, in one call — then summarise only those.” Claude writes the questions, Jev answers in well under a second, and Claude reads the probabilities instead of re-reading 40 reviews.

06Intermediate

Way 3: hooks and commands that guard Claude Code

Claude Code can run shell commands. Most are harmless — run the tests, list files — but a few can’t be undone. Hooks are small scripts Claude Code runs at fixed moments: before a tool is used, after it’s used, when you submit a prompt. They are perfect for a quick Jev judgment, because they must be fast and they must return something structured.

A guard for destructive commands

A PreToolUse hook receives the command Claude wants to run as JSON on standard input.[6] Ours asks Jev two questions and, if the answers look risky, tells Claude Code to ask you before running it.

The Jev guard hook. Before Claude Code runs a Bash command, the hook sends the command to Jev with two questions: is it destructive, and how much could be lost. If Jev is unreachable the hook stays silent and Claude Code’s normal permission rules apply. If the destructive probability is at least 0.7, the blast radius is high, or Jev is unsure, the hook asks you to confirm. Otherwise the normal permission flow continues. The hook never approves anything on its own.Claude wants to runBash(command)PreToolUse hook → Jevdestructive? (Noul)blast radius? (Score, 3 levels)no key · timeout · errorstay silent (fail open)destructive ≥ 0.7or blast ≥ 1.5or Jev unsure?yespermissionDecision: “ask”you see Jev’s numbersand confirm or refusenono decisionClaude Code’s normalpermission rules apply
Fig. 8 — The guard hook. It can only add a confirmation step; it never approves anything, and it stays silent when Jev can’t help.
.claude/hooks/jev-bash-guard.mjs
#!/usr/bin/env node
// .claude/hooks/jev-bash-guard.mjs — a PreToolUse hook for Bash.
// Asks Jev whether a command is destructive. It can only ADD a confirmation
// step; it never approves anything, and it stays silent whenever Jev can't help.

const DESTRUCTIVE = 0.7; // probability that the command is hard to undo
const BLAST = 1.5; // 0 = nothing lost … 2 = a whole project or remote system
const UNSURE = 0.5; // blast-radius confidence below this means "Jev can't tell"

let raw = "";
for await (const chunk of process.stdin) raw += chunk;
const command = JSON.parse(raw).tool_input?.command ?? "";
const key = process.env.TYPESAFE_API_KEY;
if (!key || !command) process.exit(0); // fail open: normal permission rules apply

const questions = {
  destructive: {
    type: "noul",
    instructions:
      "Would running `command` delete or overwrite files, data, git history or cloud resources in a way that is hard to undo?",
    criteria: {
      true: "Deletes, overwrites, force-pushes, resets or drops something that may not be recoverable",
      false: "Reads, builds, tests, or makes changes that are easy to undo",
    },
  },
  blast_radius: {
    type: "score",
    instructions: "If `command` did the wrong thing, how much could be lost?",
    criteria: [
      "Nothing, or only a temporary file",
      "Some uncommitted local work",
      "A whole project, repository, database or remote system",
    ],
  },
};

try {
  const res = await fetch("https://api.typesafe.ai/v1/systemone", {
    method: "POST",
    headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model: "jev-1.13.0", state: { tool: "Bash", command }, questions }),
    signal: AbortSignal.timeout(3_000),
  });
  if (!res.ok) process.exit(0);
  const { answers } = await res.json();
  const p = answers.destructive.noul;
  const blast = answers.blast_radius;
  const risky = p >= DESTRUCTIVE || blast.score >= BLAST || (p >= 0.4 && blast.confidence < UNSURE);
  if (risky) {
    const reason =
      `Jev: destructive ${p.toFixed(2)}, blast radius ${blast.score.toFixed(2)}/2 ` +
      `(confidence ${blast.confidence.toFixed(2)}). Check what this deletes before allowing it.`;
    console.log(
      JSON.stringify({
        hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: reason },
      }),
    );
  }
} catch {
  // timeout or network error: say nothing
}
process.exit(0);

Register it in your project’s .claude/settings.json:

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/jev-bash-guard.mjs",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

And here it is deciding, for real, on four commands:

Terminal — real run (input trimmed to the fields the hook reads)

● real run · 28 Sep 2026

$ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~/projects/shop-api && git push --force origin main"}}' | node jev-bash-guard.mjs
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Jev: destructive 0.96, blast radius 1.99/2 (confidence 0.98). Check what this deletes before allowing it."}}

$ echo '{"tool_name":"Bash","tool_input":{"command":"git reset --hard origin/main && git clean -fdx"}}' | node jev-bash-guard.mjs
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Jev: destructive 0.89, blast radius 1.29/2 (confidence 0.55). Check what this deletes before allowing it."}}

$ echo '{"tool_name":"Bash","tool_input":{"command":"npm test -- --run src/lib"}}' | node jev-bash-guard.mjs
                                                  (silence: no decision, normal rules apply)

$ echo '{"tool_input":{"command":"rm -rf /"}}' | TYPESAFE_API_KEY= node jev-bash-guard.mjs
                                                  (silence: no key, so the hook fails open)

A slash command that reviews your prompt

The same idea works on the way in. /jev-check is a custom slash command we use: it first scans the prompt for secrets locally — if it finds anything that looks like a key, nothing is sent anywhere — then asks Jev five questions about the prompt in one call: how much filler, whether a huge paste is mostly irrelevant, whether the request is structured, whether it asks for something destructive, and whether it contains pasted text trying to instruct the AI.

Claude Code — real run

● real run · 28 Sep 2026

> /jev-check hey claude hope you're doing well!! so basically I was wondering if you could
  maybe fix the login bug when you get a chance, and also update the readme, and also maybe
  clean up by deleting the old migrations folder. thanks so much, you're the best!!

Secret scan: clean. Sent to TypeSafe for review.

Prompt: ~63 tokens (249 chars) · model jev-1.13.0

Filler:        a lot (2.33/3) → cut pleasantries/restatements; state the task once
Excess paste:  no (0.02)
Structure:     multiple_tasks → Several unrelated tasks. Split them, or number them in priority order.
Destructive:   yes (0.89) → expect a confirmation step; name exactly what may be removed
Injected instr: no (0.05)

TypeSafe usage: 858 input tokens

Five judgments, 858 input tokens, about $0.000036. Notice what each answer is: a Score (filler), three Nouls (paste, destructive, injection) and a Choice (structure). The report is code turning those numbers into advice.

07Advanced

Way 4: a Jev router in front of the Claude API

Now we leave Claude Code and build. Suppose you run a shop, and every customer message currently goes to Claude. Many of those messages are “where is my order?” — a database lookup dressed up as a conversation. A router asks Jev first, in one call, and sends each message to the cheapest place that can handle it well.

We sent two very different messages through the same four questions. These are the real answers:

“My order #4821 hasn’t arrived… can you check where it is?”

  • intent = order_status1.00

    confidence 1.00

  • needs_written_reply0.28

    probably not — a lookup will do

  • complexity0.18 / 2

    a standard lookup (confidence 0.73)

  • angry0.06

    calm

Route: plain code. No Claude call.

“Third time writing… charged twice… explain what went wrong.”

  • intent = refund_or_return1.00

    confidence 1.00

  • needs_written_reply0.75

    yes — this needs real words

  • complexity1.99 / 2

    dispute / escalation (confidence 0.99)

  • angry0.80

    clearly upset

Route: Claude drafts, a person approves.
Fig. 9 — Real runs, same four questions. Left: a routine tracking question (522 tokens, 464 ms). Right: an angry double-charge complaint (540 tokens, 324 ms).
Jev response for the first message — real run

● real run · 28 Sep 2026

{
  "model": "jev-1.13.0",
  "answers": {
    "intent": {
      "type": "choice",
      "choice": "order_status",
      "confidence": 1,
      "probabilities": { "product_question": 0, "other": 0, "order_status": 1, "refund_or_return": 0 }
    },
    "needs_written_reply": { "type": "noul", "noul": 0.28 },
    "complexity": {
      "type": "score",
      "score": 0.18,
      "confidence": 0.73,
      "legend": {
        "0": "A standard lookup or procedure",
        "1": "Needs some judgment or several steps",
        "2": "Unusual situation, dispute or escalation"
      },
      "probabilities": { "0": 0.83, "1": 0.16, "2": 0.01 }
    },
    "angry": { "type": "noul", "noul": 0.06 }
  },
  "usage": { "input_tokens": 522, "output_tokens": 101 },
  "latency_ms": 464
}
A Jev router in front of Claude. A customer message goes to Jev with four questions in one call: intent, needs a written reply, complexity and anger. A confident order-status question that needs no written reply is answered by plain code from the database, with no Claude call. A message with complexity above 1.5 or anger above 0.7 goes to Claude to draft a reply that a person approves. Everything else, including every message when Jev is unavailable, goes to Claude, with Jev’s notes when there are any.messagefrom a customer⚡ one Jev callintent · Choiceneeds_written_reply · Noulcomplexity · Scoreangry · Noulplain codedatabase lookup · no Claude callwhen: order_status (conf ≥ 0.8) and reply < 0.5Claude drafts, a person approvesthe hard and heated caseswhen: complexity > 1.5 or angry > 0.7Claude replieswith Jev’s notes, if anywhen: everything else — or Jev failed
Fig. 10 — The routing rules, drawn. Jev supplies judgments; the branches and thresholds are ordinary code you can read and test.

Here is the whole router in TypeScript, using both official SDKs:[7]

support/router.ts
// support/router.ts — Jev decides; code routes; Claude writes only when needed.
import Anthropic from "@anthropic-ai/sdk";
import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";

// null when the key is missing: routing is an optimisation, never a dependency
const jev = process.env.TYPESAFE_API_KEY
  ? new TypeSafeClient({ timeout: 4_000, retry: { maxRetries: 1 } })
  : null;
const claude = new Anthropic();

// Every question and threshold in one place, so a person can review them.
export const QUESTIONS = {
  intent: choice("What does the customer in `message` want?", {
    order_status: "Where an existing order is, or when it will arrive",
    refund_or_return: "Money back, a return or an exchange",
    product_question: "A question about a product before or after buying",
    other: "Anything else, including greetings and off-topic chat",
  }),
  needs_written_reply: noul(
    "Does answering `message` need a thoughtful written reply, rather than a status lookup that code can fill in from a database?",
  ),
  complexity: score("How hard is `message` to resolve?", [
    "A standard lookup or procedure",
    "Needs some judgment or several steps",
    "Unusual situation, dispute or escalation",
  ]),
  angry: noul("Is the customer in `message` angry, using hostile or strongly negative language?"),
};
const SURE = 0.8;
const HARD = 1.5;
const ANGRY = 0.7;

const SYSTEM = "You are the support assistant for an online shop. Be warm, specific and brief.";

export async function handle(message: string) {
  const judged = jev
    ? await jev
        .systemOne({ model: "jev-1.13.0", state: { message }, questions: QUESTIONS })
        .catch(() => null) // a Jev failure must not cost the customer a reply
    : null;

  if (judged) {
    const { intent, needs_written_reply, complexity, angry } = judged.answers;

    // 1. Easy and certain: plain code, no Claude call at all.
    if (intent.choice === "order_status" && intent.confidence >= SURE && needs_written_reply.noul < 0.5) {
      return { route: "code", reply: await orderStatusReply(message) };
    }

    // 2. Hard or heated: Claude drafts, a person approves.
    const notes =
      `Router notes (from Jev ${judged.model}): intent=${intent.choice} (confidence ${intent.confidence.toFixed(2)}), ` +
      `complexity=${complexity.score.toFixed(2)}/2, angry=${angry.noul.toFixed(2)}.`;
    if (complexity.score > HARD || angry.noul > ANGRY) {
      return { route: "human-review", draft: await askClaude(message, notes) };
    }
    return { route: "claude", reply: await askClaude(message, notes) };
  }

  // 3. No key, a timeout or an error: exactly the behaviour before Jev.
  return { route: "claude", reply: await askClaude(message) };
}

async function askClaude(message: string, notes?: string) {
  const response = await claude.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    // The system prompt never changes, so prompt caching keeps working;
    // Jev's notes travel in the user turn instead.
    system: SYSTEM,
    messages: [{ role: "user", content: notes ? `${notes}\n\nCustomer message:\n${message}` : message }],
  });
  return response.content
    .filter((b): b is Anthropic.TextBlock => b.type === "text")
    .map((b) => b.text)
    .join("");
}

async function orderStatusReply(message: string) {
  // Your own code: find the order number with a regex, look it up, fill a template.
  const order = /#?(\d{4,})/.exec(message)?.[1];
  return order ? `Order #${order} is with the courier. Tracking: https://example.com/t/${order}` : "Could you share your order number?";
}

Four decisions in that file are worth copying anywhere:

  • Fail open. No key, a timeout or an error returns null, and the message goes to Claude exactly as it did before Jev existed. Routing is an optimisation, never a dependency.
  • A short leash. Four seconds and one retry. If the router is slow, the customer waits for nothing.
  • Only confident, easy cases skip Claude. The shortcut needs a sure intent and a low “needs a written reply”. Anything uncertain gets the full treatment.
  • Jev’s notes ride in the user turn. The system prompt never changes, so Claude’s prompt caching keeps hitting.
08Advanced

Way 5: verify what Claude writes

Claude writes wonderful, warm replies. Occasionally, a wonderful, warm reply promises something your policy doesn’t allow. A human reviewer catches that by checking the draft against the rules. Jev can do the same check in a third of a second, before anyone sees the draft.[8]

We gave Jev a refund policy, the facts of the case, and a draft reply that sounds lovely:

Draft: “I’ve processed your refund… back within 24 hours… added a free month.”

  • promises_beyond_policy0.99

    policy says 5–7 business days and no credits

  • contradicts_facts0.98

    facts say the refund has not been issued

  • tone1.92 / 2

    warm, specific, takes responsibility (confidence 0.89)

493 input tokens · 296 ms. Verdict in code: do not send.
Fig. 11 — A real run. The draft scored 1.92 out of 2 for tone — and failed both factual checks. Sounding right and being right are different questions.

Tuning a question, for real

Then something instructive happened. We tested a much better draft — “a billing specialist is issuing your refund now; it will reach your account within 5–7 business days” — and the “contradicts facts” check still fired, at 0.86. Was Jev wrong? Not exactly: “is issuing your refund now” is close to claiming it’s done. The question was too loose. So we sharpened it, spelling out that describing an action as in progress is not claiming it is complete, and we tested it on all three drafts in one call:

“Does the draft claim a not-yet-done action is complete?”

  • bad draft (“I’ve processed your refund”)0.98

    caught

  • ambiguous draft (“is issuing your refund now”)0.56

    was 0.86 with the loose wording — now “can’t tell”

  • clear draft (“will be issued by our billing team”)0.04

    cleared

Fig. 12 — Real runs while tuning one question. The sharper wording keeps catching the bad draft and clears the clear one; the ambiguous draft lands near 0.5 — “can’t tell” — which is exactly where a person should look.

That is the real craft of working with Jev: the wording of the question is the product. Test it on good, bad and borderline examples; when a borderline case lands near 0.5, decide on purpose whether it goes to a person or back for a rewrite.

The cascade: cheap draft, check, strong redraft

Put the check inside a loop and you get a cascade. A fast model drafts; Jev checks; only failures go to a stronger model, with Jev’s flags as instructions; anything that still fails goes to a person. TypeSafe’s own extraction cookbook[9] uses the same shape — a small model, a Jev verifier, and a big reasoning model only when a check fires — to get most of the big model’s quality at a fraction of the cost.

A draft-and-verify cascade. A fast Claude model drafts. Jev checks the draft for policy breaks, false statements and tone. If every check passes, the draft is sent. If not, a stronger Claude model redrafts with Jev’s flags, Jev checks again, and anything still failing goes to a person.Claude Haikudrafts fastJev checkspolicy · facts · tonefailClaude Sonnetredrafts with the flagsJev checks againstill failing → a personpass → sendpass → send
Fig. 13 — A draft-and-verify cascade. Most drafts stop at the first green box; the strong model and the person only see the hard cases.
support/verify.py
# support/verify.py — Claude drafts, Jev checks, code decides.
import anthropic
from typesafe_sdk import Noul, NoulCriteria, Score, TypeSafeClient

claude = anthropic.Anthropic()
jev = TypeSafeClient(timeout=4.0)

CHECKS = {
    "promises_beyond_policy": Noul(
        instructions="Does `draft_reply` promise something that `policy` does not allow, such as a faster timeline or a credit?"
    ),
    "claims_completed": Noul(
        instructions=(
            "Does `draft_reply` tell the customer that an action listed as false in `facts` has already "
            "been completed? Describing an action as in progress or upcoming is not claiming it is completed."
        ),
        criteria=NoulCriteria(
            true="Says, for example, that the refund has been issued or processed when `facts.refund_issued` is false",
            false="Only says what is confirmed, what is in progress, or what will happen next",
        ),
    ),
    "tone": Score(
        instructions="How does `draft_reply` sound to an upset customer?",
        criteria=[
            "Cold, defensive or dismissive",
            "Polite but generic",
            "Warm, specific and takes responsibility",
        ],
    ),
}
BROKEN = 0.5  # a policy or fact check above this fails the draft
WARM_ENOUGH = 1.0  # tone must reach at least "polite"


def check(draft: str, policy: str, facts: dict) -> list[str]:
    """Return the names of the checks the draft fails (empty list = safe to send)."""
    r = jev.system_one(
        state={"policy": policy, "facts": facts, "draft_reply": draft},
        questions=CHECKS,
        model="jev-1.13.0",
    )
    failed = [k for k in ("promises_beyond_policy", "claims_completed") if r.answers[k].noul > BROKEN]
    if r.answers["tone"].score < WARM_ENOUGH:
        failed.append("tone")
    return failed


def draft(model: str, customer: str, policy: str, facts: dict, fix: list[str] | None = None) -> str:
    note = f"\nYour last draft failed these checks: {', '.join(fix)}. Fix them." if fix else ""
    msg = claude.messages.create(
        model=model,
        max_tokens=1024,
        system=f"You write replies for a support team. Follow this policy exactly: {policy}",
        messages=[{"role": "user", "content": f"Facts: {facts}\n\nCustomer: {customer}{note}"}],
    )
    return "".join(b.text for b in msg.content if b.type == "text")


def reply(customer: str, policy: str, facts: dict) -> tuple[str, str]:
    text = draft("claude-haiku-4-5", customer, policy, facts)  # fast first draft
    failed = check(text, policy, facts)
    if not failed:
        return "send", text
    text = draft("claude-sonnet-5", customer, policy, facts, fix=failed)  # stronger redraft
    if not check(text, policy, facts):
        return "send", text
    return "human-review", text  # still failing: a person decides

We ran the check() function in that file against the live API: the bad draft failed promises_beyond_policy and claims_completed; the clear draft passed everything.

09Advanced

Bonus: Jev as a tool inside your own agent

Way 2 gave Claude Code a Jev tool. You can do the same inside your own agent on the Claude API: define a tool, and let Claude decide when a fast judgment would help. The Anthropic SDK’s tool runner handles the loop[10] — Claude calls the tool, your function runs, the result goes back, until Claude is done.

agent/ask-jev-tool.ts
// agent/ask-jev-tool.ts — let Claude decide when a quick typed judgment helps.
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { noul, TypeSafeClient } from "@typesafe-ai/sdk";
import { z } from "zod";

const claude = new Anthropic();
const jev = new TypeSafeClient({ timeout: 4_000 });

const askJev = betaZodTool({
  name: "ask_jev",
  description:
    "Ask a fast calibrated judgment model yes/no questions about one piece of text. " +
    "Returns the probability that each statement is true. Use it to triage many items " +
    "quickly; 0.5 means the model cannot tell, not 'half true'.",
  inputSchema: z.object({
    text: z.string().describe("The text to judge"),
    statements: z.array(z.string()).min(1).max(20).describe("Each a yes/no statement about the text"),
  }),
  run: async ({ text, statements }) => {
    const questions = Object.fromEntries(statements.map((s, i) => [`q${i}`, noul(s)]));
    const { answers } = await jev.systemOne({ state: { text }, questions });
    return JSON.stringify(statements.map((s, i) => ({ statement: s, probability: answers[`q${i}`]?.noul })));
  },
});

const final = await claude.beta.messages.toolRunner({
  model: "claude-sonnet-5",
  max_tokens: 16000,
  tools: [askJev],
  messages: [
    {
      role: "user",
      content:
        "Here are 40 app-store reviews (pasted below). Use ask_jev to find the ones that report a crash " +
        "or data loss, then write a short summary for the engineering team.\n\n<reviews>…</reviews>",
    },
  ],
});

console.log(final.content);
10Advanced

The production checklist

Everything above, boiled down to nine rules we would not ship without:

✓ Pin the version

Send jev-1.13.0, not jev-latest, once thresholds are tuned. Log the model field from every response.

✓ Fail open

No key, a timeout or a 5xx must leave the feature working — the way it worked before Jev.

✓ Short leash

A 3–4 s timeout and one retry. A slow router is worse than no router.

✓ One file of questions

Questions and thresholds in one place, so a person can review the wording.

✓ Key on the server

Never in a browser bundle. The JS SDK refuses to start with a key in a browser-like runtime.

✓ Batch, then measure

Every independent question in one call. Watch usage.input_tokens and real latency.

✓ Escalate, never approve

Let Jev add friction (ask, review, redo). Keep allow-decisions in code or with people.

✓ Tune on your data

20–50 real examples before trusting any threshold. The ones in this article are starting points.

✓ Keep Claude’s cache warm

Keep Jev’s notes out of the cached system prompt — in the user turn or a message of their own — so it stays byte-identical.

Fig. 14 — Nine rules for running Jev next to Claude in production.

Two of these deserve a closer look. Pinning: jev-latest is an alias that moves when TypeSafe ships a new model; every response names the exact model that answered, so log it, tune your thresholds against a pinned ID such as jev-1.13.0, and upgrade on your own schedule. Failing open: during our research one request came back as an HTTP 520 from TypeSafe’s edge network — on the next try it worked. Every integration in this article would have carried on without Jev for that one call. Yours should too.

11Beginner

When something goes wrong

SymptomLikely causeFix
Claude Code ignores the skillIt wasn’t loaded, or it was installed for another agentInvoke /typesafe:typesafe-ai directly, run /reload-plugins, and check claude plugin list shows it enabled.
Generated code uses fields that don’t existA stale skillUpdate the marketplace and the plugin (commands in Way 1), then ask again.
The jev tool doesn’t appearThe MCP server failed to startclaude mcp get jev should say Connected. Run the server by hand: a syntax error shows up at once.
Tool says “No TypeSafe key”Desktop apps don’t read your shell profileUse the Keychain lookup in the server, or pass the key with claude mcp add -e.
HTTP 401Missing or wrong keyCheck the variable is set in the shell that started Claude Code; create a new key if needed.
HTTP 422A malformed questionThe body names the field. Usual suspects: a Score with one level, a Choice with no criteria.
HTTP 429 / 529 / 5xxRate limits, overload, or the networkThe SDKs retry with backoff. In hooks and routers, fail open instead of waiting.
Answers near 0.5Jev can’t tell — often the question is ambiguousSharpen the wording or add criteria. If it is genuinely unclear, that case belongs to a person.

And the most common thinking mistake: reading a Noul of 0.5 as “half true”. It means Jev can’t tell. For “how much”, use a Score. For “which one”, use a Choice. For “is it true”, use a Noul — and read it as a probability.

12Beginner

Your first week

You don’t need all five ways on day one. Here is a path that builds up one habit at a time:

  1. Day 1

    Playground

    Paste ten real messages from your work. Write three questions. Watch what comes back.

  2. Day 2

    Key + skill

    Save the key safely, install the TypeSafe plugin, ask Claude Code where Jev fits in one project.

  3. Day 3

    MCP tool

    Add the jev server to Claude Code and let Claude triage a messy list for you.

  4. Day 4

    A safety net

    Install the Bash guard hook or /jev-check. Notice when it speaks up — and when it should have.

  5. Day 5–6

    Router or verifier

    Put one call in front of, or behind, a real Claude feature. Fail open. Log everything.

  6. Day 7

    Tune

    Label 30 cases, move the thresholds, pin the model version, write down why.

Fig. 15 — Seven days from a first Playground run to a tuned integration.

Quick quiz

Tap a question to check your answer.

Q1Can you set Claude Code’s model to Jev?

No. Jev doesn’t generate text or code. You use it alongside Claude: Claude Code can write code that calls Jev (Way 1), call it as a tool (Way 2), or be guarded by it (Way 3).

Q2Why does the Bash guard hook only ever return “ask”, never “allow”?

Because a model’s judgment should add friction, not remove it. If Jev is wrong, the worst case is an extra confirmation; Claude Code’s normal permission rules still apply to everything else.

Q3Your router’s Jev call times out. What should happen?

The message goes to Claude exactly as it did before you added Jev. Fail open: routing is an optimisation, never a dependency.

Q4A draft scores 1.92 out of 2 for tone. Is it safe to send?

Not on that basis. Tone and truth are separate questions. In our real run, a 1.92-tone draft failed both the policy and the facts checks.

Q5A check returns 0.56 on a borderline draft. What now?

It means “can’t tell”. Sharpen the question and test again on good, bad and borderline examples — and decide deliberately whether such cases go to a person.

Claude thinks. Jev judges. Code decides. Put the receptionist at the front desk, keep the specialist for the work only a specialist can do, and write the rules between them where you can read them. That’s the whole trick — and now you have five ways to do it. ⚡🧠

New to Claude itself? Start with our guide to using Claude. New to Jev? Read TypeSafe and Jev first — this article builds on it.

References

The sources behind this article: original papers, official announcements and documentation. Each link was opened and checked on ; documentation without a fixed date is cited as it read that day.

  1. [1]

    TypeSafe AI. API reference. TypeSafe documentation.

    Documentation · Supports: Request and response shapes, error codes

  2. [2]

    TypeSafe AI. Jev with coding agents. TypeSafe documentation.

    Documentation · Supports: Why Jev is not a model for Claude Code itself

  3. [3]

    TypeSafe AI. Models. TypeSafe documentation.

    Documentation · Supports: Price, limits, aliases and pinning

  4. [4]

    TypeSafe AI. Agent skill. TypeSafe documentation.

    Documentation · Supports: Way 1: installing and using the skill

  5. [5]

    Model Context Protocol (2025). Specification (version 2025-06-18). modelcontextprotocol.io.

    Documentation · Supports: Way 2: the MCP server

  6. [6]

    Anthropic. Hooks reference. Claude Code documentation.

    Documentation · Supports: Way 3: PreToolUse input and permission decisions

  7. [7]

    TypeSafe AI. JavaScript SDK. TypeSafe documentation.

    Documentation · Supports: The router code

  8. [8]

    TypeSafe AI. Guardrails for LLMs. TypeSafe cookbooks.

    Documentation · Supports: Way 5: checking inputs and outputs

  9. [9]

    TypeSafe AI. SDE cascade. TypeSafe cookbooks.

    Documentation · Supports: The cascade pattern

  10. [10]

    Anthropic. Tool use with Claude. Claude Platform documentation.

    Documentation · Supports: The Jev tool inside your own agent

How this article was made

Researched and drafted with the help of AI assistants, checked against the sources above, and published by Mrinal Singh Raja as general information, not professional advice. Facts come from the sources listed and may change, so please check them before relying on them. Spotted a mistake? Tell us and we will correct it. Our editorial standards explain how AI is used here.

TypeSafe and Jev are products of TypeSafe AI; Claude, Claude Code and Claude Desktop are products of Anthropic. This article is independent and not affiliated with or endorsed by either. API shapes, SDK versions, prices, limits and the agent-skill commands follow docs.typesafe.ai; hook behaviour follows Claude Code’s hooks documentation; both were read on 28 September 2026. Probabilities, token counts and latencies are from real runs on that date and will differ on yours. The MCP server, the hook and /jev-check are our own examples, not official TypeSafe or Anthropic tools. Spotted something out of date? Tell us.