⚡ Claude Code × Jev Guardrails

September 20, 2026 · View on GitHub

Claude thinks. Jev reacts. Code decides.

A real-time reflex layer for Claude Code that hooks into the agent lifecycle, asks TypeSafe's Jev System One model a batch of small, calibrated yes/no questions about each proposed or completed action, and runs those probabilities through a tiny deterministic policy engine that can allow / warn / block / ask — before anything irreversible happens.

Jev never picks the plan and never writes the intervention text. It only answers "how likely is this?" questions. Every ALLOW/WARN/BLOCK/ASK_USER decision, every threshold, and every user-facing message is plain, auditable, deterministic code.

CI License: MIT Node No build step


Why this exists

Most "LLM-as-a-judge" guardrails ask a general-purpose chat model to reason in free text about whether an action is safe, then parse its answer. That pattern has well-documented failure modes:

  • Calibration drift nobody notices. A team shipped an LLM-as-judge for groundedness using GPT-4 to judge GPT-4's own outputs. Dashboards stayed green for three months; a manual audit found the actual expert-judge agreement (κ) was 0.31 — the judge had been quietly rubber-stamping its own model family the whole time.
  • Judges aren't uniformly reliable even at the frontier. RAND's Judge Reliability Harness found frontier LLM judges clear >80% accuracy in controlled benchmarks but exceed 50% error rates on bias stress-tests in production — position bias, verbosity bias, and self-preference bias all measurably skew free-text verdicts.
  • Latency and cost fight against inline blocking. A judge call typically costs 100ms-2s; a blocking guardrail on the hot path needs to stay under ~200ms. Running a frontier judge on every single tool call in an agent loop is usually too slow and too expensive to do at all, so most guardrails only sample or run async, after the fact.

Jev is a different kind of model: a "System One" model that returns typed, calibrated probabilities instead of generated text, no free-form reasoning to be swayed by verbosity or position, no parsing a judge's prose to extract a verdict. It's trained with RLCD (Reinforcement Learning for Calibrated Decisions) instead of RLHF/RLVR, specifically to make its probabilities mean what they say (a 0.8 should be right 80% of the time). And because it just returns numbers, it's fast (70-500ms) and cheap enough ($0.0004/call) to run 5-8 questions on every tool call, not just a sampled subset.

This project treats that distinction as an architectural boundary, not just a model swap:

Typical LLM-as-a-judgeThis project
OutputFree text, then parsedTyped probability per question (Noul)
Who decides ALLOW/WARN/BLOCKThe model's own reasoningDeterministic code (policy/engine.ts), fixed thresholds
Hard safety cases (secrets, destructive commands)Depend entirely on the modelRegex-based deterministic floor, combined via Math.max, never overridden by the model
If the judge is unreachableUsually fails the whole guardrailFails open for judgment calls, fails closed for hard cases
Cost/latency budgetOften too slow/expensive to run inline on every actionCheap and fast enough to run on every hook

Jev supplies probabilities. The policy engine supplies judgment. Neither one does the other's job.


Architecture

flowchart LR
    A[Claude Code agent loop] -- hook event --> B[Hook script]
    B --> C[State builder<br/>redacted, concise text]
    C --> D[Jev /v1/systemone<br/>Noul questions]
    D -- calibrated probabilities --> E[Deterministic policy engine<br/>fixed thresholds]
    F[Deterministic hard floors<br/>secret pattern / destructive pattern /<br/>exact-repeat count] -- Math.max --> E
    E -- ALLOW / WARN / BLOCK / ASK_USER --> B
    B -- hookSpecificOutput --> A
src/reflex/
  config.ts               named, env-tunable policy thresholds + mode
  jev-client.ts            real Jev HTTP client (POST /v1/systemone)
  jev-client-mock.ts        offline heuristic stand-in (JEV_MODE=mock / no key)
  client-factory.ts         picks real vs mock client
  jev-types.ts               Jev request/response/question types
  redact.ts                  deterministic secret redaction (hard safety net)
  summarize.ts                tool call/result -> short, redacted, one-line summaries
  extract-constraints.ts      heuristic "do not X" extraction from prompts
  state-builder.ts             SessionState -> concise text sent to Jev
  state/
    types.ts                   SessionState / ReflexEvent / ActionRecord types
    store.ts                    append-only JSONL event log + fold into SessionState
    fingerprint.ts               stable fingerprint for exact-repeat detection
  signals/
    pre-tool.ts                 8 Noul questions -> PreToolSignals (+ fail-open)
    post-tool.ts                 6 Noul questions -> PostToolSignals
    completion.ts                 5 Noul questions -> CompletionSignals
  policy/
    engine.ts                     pure signals -> {ALLOW,WARN,BLOCK,ASK_USER}
    types.ts
  logger.ts                       terminal panels + reflex.log
  hooks/
    user-prompt-submit.ts          captures goal + constraints
    pre-tool-use.ts                 the main gate: constraint / secret / destructive / loop reflexes
    post-tool-use.ts                 records outcome/evidence, can WARN via stderr
    stop.ts                          completion reflex, can BLOCK the stop
  demo/simulate.ts                   deterministic, offline, end-to-end demo of all 3 reflexes
demo-fixture/                 tiny separate Node project with one real bug, for a live recording
tests/                        policy engine + state store tests (node:test)

State is never the full Claude transcript. Each session gets its own append-only JSONL log at .claude-reflex/state/<session_id>.jsonl (goal, constraints, recent actions/failures, evidence, verifications, interventions) folded into a small SessionState on each hook invocation.


Seen it block a real action

This isn't just a diagram, this is an actual panel captured from .claude-reflex/reflex.log during development, in REFLEX_MODE=enforced, against the real Jev API. Claude proposed a command that read as destructive (deleting a "legacy database export"); the deterministic pattern floor in signals/pre-tool.ts forced the destructive-action probability to its 0.9 ceiling regardless of Jev's own score, the policy engine compared it against the configured 0.75 block threshold, and Claude Code denied the tool call before it ever reached a shell:

⚠️  DESTRUCTIVE ACTION REFLEX FIRED

Constraint violation:     0.09
Secret exposure:          0.06
Destructive/irreversible: 0.90

→ BLOCKED
Reason: This action looks destructive or irreversible and is not clearly necessary for the goal.

The command's actual target didn't even exist on disk, this repo's own safety net treats "looks destructive" as reason enough to stop and ask, not just "provably would destroy something." (The flip side of that same sensitivity: the pattern floor is a plain string match, so it can also fire on documentation that merely mentions a dangerous command pattern. It's a deliberately blunt, auditable backstop, not a substitute for the calibrated Jev signal it's combined with.)


Setup

Requires Node >= 22.18 (uses Node's built-in TypeScript type stripping, enabled by default since Node 22.18/23.6, no build step, no ts-node/tsx).

git clone https://github.com/javimp2003/claude-code-jev-guardrails.git
cd claude-code-jev-guardrails
npm install
cp .env.example .env   # then set JEV_API_KEY (or leave empty to use the mock client)
npm run typecheck
npm test

Configuring the Jev API key

Set JEV_API_KEY in .env (loaded automatically by every hook, Claude Code does not source your shell's .env). Get a key at https://typesafe.ai. If JEV_API_KEY is unset (or JEV_MODE=mock is set), the Reflex Layer automatically falls back to an offline heuristic mock client so it still runs without network access, used by npm run demo.

Wiring the hooks into a real Claude Code session

.claude/settings.json in this repo already registers all four hooks for this project itself. To use the Reflex Layer in another project, copy the hooks block from .claude/settings.json into that project's own .claude/settings.json, replacing ${CLAUDE_PROJECT_DIR} usage as needed (or use an absolute path, see demo-fixture/.claude/settings.json for that pattern).

Shadow vs. enforced mode

REFLEX_MODE=shadow    # default: every reflex evaluated + logged, nothing ever blocked
REFLEX_MODE=enforced  # policy decisions actually allow/warn/deny/ask

Shadow mode panels print WOULD_BLOCK/WOULD_WARN instead of BLOCKED/WARN and are annotated (shadow mode, not enforced). Start new integrations in shadow mode, watch the log for a while, then flip to enforced once the thresholds feel right for your workflow.


Running the demo

The deterministic, offline demo drives the real hook scripts (as subprocesses, exactly like Claude Code would) through a scripted sequence that reliably reproduces all three reflexes:

npm run demo

In a second terminal, for a live tail during a recording:

tail -f .claude-reflex-demo/reflex.log

Expected sequence (see src/reflex/demo/simulate.ts):

  1. Goal + constraint ("do not modify tests") captured.
  2. Editing the test file → ⚡ CONSTRAINT REFLEX blocks it.
  3. A different (buggy) strategy is attempted and fails twice.
  4. The exact same strategy attempted a 3rd time → ⚡ LOOP REFLEX blocks it.
  5. The real fix (npm install bcrypt) is applied successfully.
  6. Claude tries to stop without running tests → 🛑 DONE REFLEX blocks the stop.
  7. Tests are run and pass → stop is allowed (✓ Completion allowed).

For an optional live recording inside an actual Claude Code session instead of the scripted simulation, see demo-fixture/PROMPT.md.


Configurable thresholds

All in src/reflex/config.ts, overridable via env var (see .env.example for the full list and defaults): REFLEX_THRESH_CONSTRAINT_VIOLATION_BLOCK, REFLEX_THRESH_SECRET_EXPOSURE_BLOCK, REFLEX_THRESH_DESTRUCTIVE_BLOCK/_WARN, REFLEX_THRESH_LOOP_PROBABILITY_BLOCK, REFLEX_THRESH_LOOP_EXPECTED_PROGRESS_CEILING, REFLEX_THRESH_REDUNDANT_WARN, REFLEX_THRESH_EVIDENCE_INSUFFICIENT_WARN, REFLEX_THRESH_GOAL_ALIGNMENT_WARN, REFLEX_THRESH_POST_TOOL_BLOCKER_WARN, REFLEX_THRESH_POST_TOOL_RETRY_UNLIKELY_WARN, REFLEX_THRESH_UNRESOLVED_FAILURE_BLOCK, REFLEX_THRESH_VERIFICATION_INSUFFICIENT_BLOCK, REFLEX_THRESH_MORE_VERIFICATION_HELPFUL_BLOCK. Also REFLEX_HARD_LOOP_REPEAT_COUNT (deterministic exact-repeat floor, independent of Jev) and REFLEX_RECENT_ACTION_WINDOW (state size).


Safety notes

  • Fail open: if Jev is unreachable/times out/errors, hooks fall back to permissive defaults for ordinary judgment calls (pre-tool.ts, post-tool.ts, completion.ts each have a fallbackSignals()), so a Jev outage never wedges the agent.
  • Fail closed for hard safety cases: obvious secret patterns (redact.ts) and obvious destructive command patterns are checked deterministically and combined with (never overridden by) Jev's own probability, so those two checks work even if Jev is down.
  • Never logs secrets: redact.ts is applied to every string before it is sent to Jev, appended to the state log, or printed to a terminal.
  • Loop detection has a deterministic floor (countUnresolvedRepeats in state/store.ts): an exact-fingerprint repeat count is combined with Jev's repeats_failed_approach_without_new_evidence probability via Math.max, so the loop reflex cannot be talked out of firing by Jev being either unavailable or miscalibrated on an exact repeat.
  • The Stop hook has a one-shot safety valve: it will not block completion twice in a row for the same session (Claude Code's own stop_hook_active re-entrancy flag), so a Reflex Layer disagreement with Claude cannot create an infinite stop loop.

What's been verified

  • Live Jev API calls: exercised against the real POST https://api.typesafe.ai/v1/systemone endpoint with a real API key, auth, request/response schema, and retry path all behave as implemented.
  • Live, enforced blocking inside a real Claude Code session: with REFLEX_MODE=enforced and a real API key, the PreToolUse hook denied a live tool call end-to-end (Claude Code never executed it), see the captured panel above.
  • Deterministic offline demo and unit tests: npm run demo and npm test (20 tests) exercise the policy engine and state store against the mock client and pass reliably with no network access.

Not yet verified: sustained/production-scale usage, concurrent multi-session load on the JSONL state store, and adversarial red-teaming of the Jev questions themselves (e.g. prompt-injection attempts to manipulate Noul answers). Treat this as a working prototype, not a hardened production guardrail.


License

MIT