⚡ 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.
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-judge | This project | |
|---|---|---|
| Output | Free text, then parsed | Typed probability per question (Noul) |
| Who decides ALLOW/WARN/BLOCK | The model's own reasoning | Deterministic code (policy/engine.ts), fixed thresholds |
| Hard safety cases (secrets, destructive commands) | Depend entirely on the model | Regex-based deterministic floor, combined via Math.max, never overridden by the model |
| If the judge is unreachable | Usually fails the whole guardrail | Fails open for judgment calls, fails closed for hard cases |
| Cost/latency budget | Often too slow/expensive to run inline on every action | Cheap 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):
- Goal + constraint ("do not modify tests") captured.
- Editing the test file → ⚡ CONSTRAINT REFLEX blocks it.
- A different (buggy) strategy is attempted and fails twice.
- The exact same strategy attempted a 3rd time → ⚡ LOOP REFLEX blocks it.
- The real fix (
npm install bcrypt) is applied successfully. - Claude tries to stop without running tests → 🛑 DONE REFLEX blocks the stop.
- 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.tseach have afallbackSignals()), 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.tsis 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 (
countUnresolvedRepeatsinstate/store.ts): an exact-fingerprint repeat count is combined with Jev'srepeats_failed_approach_without_new_evidenceprobability viaMath.max, so the loop reflex cannot be talked out of firing by Jev being either unavailable or miscalibrated on an exact repeat. - The
Stophook has a one-shot safety valve: it will not block completion twice in a row for the same session (Claude Code's ownstop_hook_activere-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/systemoneendpoint 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=enforcedand a real API key, thePreToolUsehook 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 demoandnpm 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.