Skip to main content

reeflex-claude

Reference adapter: the Claude Code PreToolUse hook — Reeflex governance that also protects you from your own agent, gating every tool call on its impact before it runs.

What it is: A Reeflex adapter that governs Claude Code tool calls by implementing the four contract responsibilities (SPEC §6): INTERCEPT → NORMALIZE → ENFORCE → AUDIT.

What it is NOT: It does not decide anything. The decision is made deterministically by reeflex-core (OPA/Rego). Zero LLM anywhere near the decision path.

How it works

Claude Code fires a PreToolUse hook before every tool call. This adapter:

  1. INTERCEPT — receives the tool call JSON on stdin (before execution).
  2. NORMALIZE — maps the tool call to a signed Action Envelope (SPEC §2): verb, three risk axes (reversibility / blast_radius / externality), tier.
  3. ENFORCE — POSTs the envelope to reeflex-core /v1/decide; maps the Decision to Claude Code's permissionDecision (allow | deny | ask).
  4. AUDIT — appends one JSONL record per decision to the audit log.

Fail-closed invariant: if core is unreachable for any reason, the hook emits deny and exits 0. It NEVER exits non-zero (which would make Claude Code continue the tool anyway — silent allow).

flowchart LR
    T["Claude Code tool call<br/>(Bash, Write, Edit, …)"] --> H["PreToolUse hook<br/><i>this adapter</i>"]
    H -- "Action Envelope" --> C["reeflex-core<br/>POST /v1/decide"]
    C --> D{Decision}
    D -- allow --> R["✅ Tool runs"]
    D -- ask --> A["✋ Human confirmation dialog"]
    D -- deny --> X["⛔ Tool blocked, reason fed to the model"]
    style D fill:#f6f8fa,stroke:#57606a

If your Reeflex portal gave you a line (connect)

Skip the three steps below. A Reeflex portal's Connect an agent screen (/app/onboard) hands you one line:

pip install 'reeflex-claude>=0.2.0' && reeflex-claude connect \
  --gate <gate-id> --token <rfx_reg_...> --portal https://app.reeflex.io

It exchanges the registration token for that gate's configuration, writes one agent config, asks your engine one real question, and reports the verdict back to the portal — then stops. --agent claude (default) writes the Claude Code hook; --agent opencode writes an OpenCode plugin; --agent litellm writes a proxy config fragment. --dry-run prints every path it would write and exits, exchanging nothing.

Three things worth knowing before you paste it:

  • It does not fetch a document and execute it. The whole procedure is written out in docs/setup/v1/agent-setup.md, the portal shows that file in full beside the copy button with its SHA-256, and this package pins the same digest. "Fetch a URL and follow whatever it says" is the instruction shape Reeflex exists to gate; we do not issue it.
  • The token in the line is not your gate token. It is scoped to one gate, spent by one exchange, and expires in minutes. The exchange returns no secret. If your engine needs a bearer token, export REEFLEX_CORE_TOKEN yourself first — connect reads it from the environment, says so when it is absent, and never invents one.
  • What the portal then shows is your agent's report. The /v1/decide round trip is real; the report of it is not signed evidence. Signed decision records come from the evidence connector, which this does not install.

Install / wire up

1. Install. Requires Python 3.8+ (and a recent pip — upgrade with python3 -m pip install --upgrade pip if the install fails on an old box).

pip install reeflex-claude

2. Have a reachable core. Either run reeflex-core locally (docker compose up -d from the repo root gives you http://127.0.0.1:8080) or point at an existing deployment.

3. Wire the hook and verify it, in two commands:

reeflex-claude setup   # writes the fail-closed PreToolUse hook into .claude/settings.json
reeflex-claude check   # verifies the deny path: fails closed if core is unreachable

setup targets the current project's ./.claude/settings.json by default (created, with parent directories, if absent); pass --global to target ~/.claude/settings.json instead. It merges — an existing settings file keeps every unrelated key; only the reeflex-claude hook PreToolUse entry and the REEFLEX_* keys in the env block are written or updated. A corrupt existing settings.json is left untouched and setup exits 1 with an explanation — it never guesses or overwrites.

Run setup in an interactive terminal (TTY) to be prompted for each value below; in a non-interactive or piped context (CI, scripts, no TTY) it silently skips the prompts and uses the defaults shown — no error, no hang.

Non-interactive flags (all optional; omitted ones fall back to an interactive prompt when running in a terminal, otherwise to the default shown):

Flag Default Meaning
--core-url http://127.0.0.1:8080 reeflex-core endpoint
--token none optional bearer token — prefer REEFLEX_CORE_TOKEN via env instead; see the warning setup prints if you use this flag
--verify-ssl true false only for dev/self-signed endpoints, at your own risk
--mode enforce (fail-closed) observe = fail-open calibration mode (recommended for a first dry run)
--env production production | staging | dev

Expected reeflex-claude setup output (abridged):

[reeflex-claude] Wrote new PreToolUse hook entry in /path/to/.claude/settings.json
[reeflex-claude]   matcher: *
[reeflex-claude]   command: /path/to/venv/bin/reeflex-claude hook  (timeout 30s)
[reeflex-claude]   env: {"REEFLEX_CORE_URL": "http://127.0.0.1:8080", "REEFLEX_MODE": "enforce", ...}
[reeflex-claude] Mode: ENFORCE (fail-closed) -- the safe default for a governance gate.
[reeflex-claude] Now run: reeflex-claude check

Expected reeflex-claude check output (PASS):

[reeflex-claude] probing hook command: ['/path/to/venv/Scripts/reeflex-claude', 'hook']
======================================================================
PASS -- fail-closed verified
======================================================================
hook denied the probe and exited 0 (fail-closed verified). stdout=...
[reeflex-claude] settings OK: /path/to/.claude/settings.json contains the reeflex-claude PreToolUse hook.

check forces the probe's own REEFLEX_MODE=enforce and points it at an unreachable core address regardless of your real configuration — it is testing the adapter's fail-closed plumbing (installed correctly, resolvable on PATH, denies when core is unreachable), not your policy configuration (that is core's job, exercised by demo/run_demo.py or a real /v1/decide call). Exit code 0 = PASS, 1 = FAIL.

Why the wired command is an absolute path. A PreToolUse hook that cannot start exits non-zero, and Claude Code then runs the tool anyway — a silent allow, with no audit record and no message on screen. Earlier releases wired the bare name reeflex-claude hook, which only moved that risk from the cwd to the PATH: install into a virtualenv, launch Claude Code from an ordinary shell, and the hook is command not found. setup therefore writes the absolute path of the installed entry point, and check verifies the command that is actually in your settings.json.

Why the matcher is *. Claude Code treats matcher as an allowlist: a tool whose name does not match never reaches the hook at all. Listing tool names would silently exempt every mcp__* tool — a database, payments or cluster MCP server is exactly where an irreversible production action lives — along with Task, SlashCommand, Skill, and every tool added to Claude Code after the list was written. A governance gate cannot be enumerated in advance; unknown tools are classified conservatively instead (see classify.py). If you deliberately narrow the matcher, check will say so.

4. Restart Claude Code. From the next tool call on, every action passes through the gate.

Development install

For contributors working from a git clone (no pip install), the adapter is plain Python (stdlib only) and lives in this repository:

git clone https://github.com/Reeflex-io/reeflex.git
cd reeflex/reeflex-claude

Wire it into Claude Code's settings.json (a full sample is in examples/settings.sample.json) using the absolute path to hook_entry.py so it works regardless of the working directory Claude Code runs from:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "python /absolute/path/to/reeflex/reeflex-claude/hook_entry.py",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

(python -m reeflex_claude also works, but only if Claude Code's working directory is reeflex-claude/ — the absolute-path form is the reliable one.)

⚠ A misconfigured hook fails OPEN. Claude Code only treats a PreToolUse hook's exit code 2 as "block". Any other non-zero exit (for example, the No module named reeflex_claude you get from python -m reeflex_claude when the working directory is not reeflex-claude/) is treated as a probe failure, and Claude Code runs the tool anyway — the gate is silently bypassed. hook_entry.py is written to always exit 0 and print an explicit allow/deny/ask decision, so it fails closed instead. After wiring the hook, always run the verify command below before trusting it. (This is exactly the failure class that the pip-installed reeflex-claude check command above structurally eliminates — no absolute paths, no cwd-dependent import.)

Verify the wiring. Run this once, with the same absolute path you put in settings.json (core does not need to be running — the point of this check is that a destructive command gets an explicit deny, not a crash):

echo '{"session_id":"verify-1","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | python /ABSOLUTE/PATH/TO/reeflex/reeflex-claude/hook_entry.py

Expected stdout (one JSON line; the exact wording of the connection error varies by OS):

{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Reeflex: core unreachable or error -- failing closed: ... [rule=reeflex.core/fail_closed]"}}

Then check the exit code — it MUST be 0 even though the decision is deny (that is fail-closed working correctly; a non-zero exit here is exactly the fail-open bug described above):

# POSIX (bash/zsh)
echo $?

# Windows (cmd.exe)
echo %ERRORLEVEL%

# Windows (PowerShell)
$LASTEXITCODE

If you instead ran the unqualified python -m reeflex_claude form from a directory other than reeflex-claude/, this same check will show the failure mode directly: No module named reeflex_claude printed to stderr and a non-zero exit — confirming the fail-open gap this section warns about.

Set the environment variables (below) and restart Claude Code, or run reeflex-claude setup from within the git clone (it works the same way — pip install -e . first, or run it via python -m reeflex_claude.cli setup).

Environment variables

Variable Default Purpose
REEFLEX_CORE_URL http://127.0.0.1:8080 reeflex-core endpoint
REEFLEX_CLAUDE_ENVIRONMENT production target environment (production|staging|dev)
REEFLEX_CLAUDE_STRICT unset if set truthy: unknown execute → irreversible and broad, so an unrecognised command in production reaches a human. Before RFX-145 it lifted only reversibility, and R2 requires broad, so it could not change any verdict — it changed a word in the audit log.
REEFLEX_CLAUDE_PRINCIPAL null on_behalf_of value in the envelope
REEFLEX_CLAUDE_AUDIT_LOG <tempdir>/reeflex-claude-audit.jsonl adapter-side audit log path
REEFLEX_CLAUDE_TIMEOUT 5 HTTP timeout to core in seconds
REEFLEX_VERIFY_SSL true (full TLS verification) set to 0/false/no/off (case-insensitive) to disable TLS certificate verification on the call to core. Insecure — dev/self-signed endpoints only, at the operator's own risk. Same env name as the WordPress adapter.
REEFLEX_CORE_TOKEN unset optional bearer token; when set, adds Authorization: Bearer <token> to the /v1/decide request. Never logged. Same env name as the WordPress adapter.

Setting REEFLEX_CLAUDE_ENVIRONMENT=dev or staging relaxes the base policy (R2/R3/R6 are production-scoped), letting dev workflows through without approvals.

reeflex-claude setup writes these into your Claude Code settings.json env block for you (see "Install / wire up" above); the table is the reference for what each variable does and for the git-clone / manual-export path.

Trying it against api-dev.reeflex.io

https://api-dev.reeflex.io is a public evaluation endpoint. Use the public eval token below — no signup needed:

Eval token: reeflex-eval-public-2026 — dev endpoint, rate-limited, may reset anytime; not for production.

reeflex-claude setup --core-url https://api-dev.reeflex.io \
  --token reeflex-eval-public-2026 --mode observe
reeflex-claude check

TLS verification stays on (--verify-ssl defaults to true) — no flag is needed here: api-dev.reeflex.io carries a valid, publicly-trusted certificate. Starting with --mode observe lets you watch decisions land in the audit log without the adapter enforcing them, so a policy misconfiguration or connectivity issue can't block your Claude Code session. Note that reeflex-claude check always probes with an intentionally unreachable core address regardless of --core-url — it verifies the adapter's fail-closed plumbing, not connectivity to api-dev itself; use the demo or a manual curl .../v1/decide to confirm the endpoint responds.

For the git-clone / manual-export path, the equivalent is:

export REEFLEX_CORE_URL=https://api-dev.reeflex.io
export REEFLEX_CORE_TOKEN=reeflex-eval-public-2026   # public eval token (may reset anytime)
export REEFLEX_MODE=observe                 # recommended for a first run — see below

Decision mapping

core decision permissionDecision effect
allow allow tool runs
deny deny tool blocked; reason fed to model
require_approval ask human confirmation dialog shown
core unreachable deny fail-closed; reason explains the error

Running the demo

set REEFLEX_OPA_BIN=C:\path\to\opa.exe
python reeflex-claude/demo/run_demo.py

Runs 7 scenarios (ls → allow, rm -rf / → deny, force push → ask, fragmentation → ask at budget, fail-closed → deny exit 0).

Running the tests

cd reeflex-claude
python -m unittest discover -s tests -v

No network required for unit tests (classify + envelope are pure; enforce tests spin a local stub server). test_setup_settings.py and test_cli.py cover setup's merge semantics (fresh write, merge-preserving, corrupt-JSON refusal) and check's deny-scenario probe (healthy install, missing binary, non-zero exit, malformed output, wrong decision) without touching your real ~/.claude or ./.claude directories.

test_conformance_bash.py runs the corpus in reeflex_claude/conformance.py — commands whose real-world effect was written down before the classifier was — through the classifier and an offline transcription of R1–R4, and fails if any irreversible production destruction reaches allow, or if any everyday command stops reaching it. The live equivalent of the same corpus, through the real hook against a real core, is:

python3 scripts/attack-probe-rfx144-agent-prices-own-action.py --strict --budget

Its exit code is the number of ground-truth production destructions that were allowed with no human, so CI can gate on it directly. Known residuals are printed with the ticket that tracks them and excluded from the count — a bounded gate that says what it does not cover is honest; one that silently drops cases from its own total reads as "covered everything".

Limits / upgrade paths

  • Bash classification is heuristic — structural matching on each command of the line, not a shell AST. Since RFX-144 the whole line is classified and not its first token: it is split at &&, ||, ;, |, & and newline (quote-aware), sh -c '<inner>' is expanded in place, sudo/env/timeout/nohup/xargs are peeled off, every resulting command is classified on its own, and the most dangerous one is reported. A line is a read only when every command on it is a read. Known gaps today, none of them closed by this design: a command inside a script file (python3 deploy.py, bash deploy.sh) is opaque, and a destruction executed on another host (ssh prod rm -rf /srv/data) is priced as the outbound emit it is rather than the delete it causes. UPGRADE: a real shell-AST parser once tooling stabilises.

  • A single named production file: covered two ways now, and the two do not cover the same set (RFX-153). blast_radius is a cardinality axis, so R2's broad requirement never reached rm /srv/prod/db.sqlite — one production database, gone. Adapter side: a path whose name says it is a container of records (.sqlite, .db, .sql, .dump, .tar*, /pgdata/, /mysql/ …) is now raised to broad, and a block device (/dev/sdb, excluding /dev/null and friends) to systemic. This is a KIND claim, which SPEC §4.2 permits a name to make, and it is raise-only — the cardinality is still 1, and it can never make an enumerated set smaller or lower a systemic reading. So rm /srv/prod/db.sqlite reaches a human while rm /tmp/scratch.txt and rm /srv/app/notes.txt still allow. What the adapter side does NOT cover: a production file whose name carries no container signal — /home/app/data/store, /srv/prod/state. The adapter cannot price those broad without claiming a cardinality it cannot observe. Policy side: R6 (SPEC §4.3) closes that remainder — it holds an irreversible production action on a declared production asset at any cardinality, by ref rather than by name shape. Read this before assuming you are covered: the list shipped in reeflex-core/policy/protected.rego is a floor derived from the Filesystem Hierarchy Standard (/srv, /var/lib, /var/opt, ...). It knows nothing about /home/app/data, an unconventional database directory, or your bucket names. UPGRADE: add your paths to protected_assets, or set default_protected := true to hold every irreversible production action whose ref is not declared ephemeral.

  • Two destruction shapes needed BOTH halves of this change (RFX-153 + RFX-144, measured). > /srv/prod/db.sqlite and dd if=/dev/zero of=/srv/prod/db.sqlite used to be classified execute + recoverable, and R6 requires irreversible — so while the policy pack alone was in place, no policy posture rescued them: a policy rule cannot correct an envelope that under-declares irreversibility. Both are now delete + irreversible + broad. Measured through the shipped policy pack, same envelope both ways:

    adapter axes decision
    execute + recoverable (before RFX-144) allow / reeflex.policy/default_allow
    delete + irreversible (after RFX-144) require_approval / reeflex.policy/irreversible_protected_asset_prod

    Stated this way on purpose: neither change closed this on its own, and each was green in its own test suite for the whole time the gap was open. If only one of the two is present in your build, treat redirection and dd as ungoverned.

  • Stub signing: meta.signature = "ed25519:stub:...". UPGRADE: Vault-backed ed25519 signing once the key management path is implemented (SPEC §6 note).

  • Four families the classifier cannot see at all (RFX-158): the program arrives on stdin (curl … | sh), the program is in a file (bash deploy.sh, python3 deploy.py), the command word is expanded by the shell ($RM -rf …, $(echo rm) …), or the destruction runs on another host (ssh prod 'rm -rf /srv/data', priced as the outbound emit it is). The destruction is not in the command string, so this classifier does not price it — and for the first three it is not in the command string at all, so nothing that reads only that string would. The ssh case is different and should not be lumped in: the remote command is on the line, and a classifier that parses ssh's argv can price it (PR #98 did, measured), so that one is unclosed work rather than a limit of the approach. REEFLEX_CLAUDE_STRICT=1 does cover five of the six — measured, not asserted: they are all unrecognised execute commands, which strict mode prices irreversible+broad. The ssh case is not one of them (it is classified emit, not unrecognised).

  • REEFLEX_CLAUDE_STRICT: unset by default so coding agents are not blocked on every npm install. When set, every UNRECOGNISED command is priced irreversible + broad, so in production it reaches a human — measured live on the conformance corpus, it moves 23 of 82 verdicts, including pytest, npm install and make build from allow to ask, and five of the six RFX-158 gaps above. That is the whole point of the knob: it is the noisy setting, and today it is the broadest lever this adapter ships for the commands the classifier cannot read. It is not the only way to cover them: a policy-side rule, or parsing the wrapper the destruction hides behind, both reach cases strict mode does not (the ssh family above is one). Before RFX-145 it was neither noisy nor safe — it moved zero verdicts and only changed a word in the audit log. UPGRADE: use a per-command allow-list in policy instead.

  • approval re-submission: the hook sets approval.present = false at interception. Re-submission with approval.present = true after human approval is the caller's responsibility (Claude Code surfaces the ask dialog; the human clicks allow; Claude Code retries the tool — the adapter then re-intercepts with the same payload, at which point the policy must be configured to allow with approval present).

Observe mode

Observe mode is a dry-run / monitoring mode. Set REEFLEX_MODE=observe in the hook's environment and the adapter will always emit allow — Claude Code proceeds with the tool call — while still writing an audit record annotated with "mode": "observe" and the would-be verdict from core.

export REEFLEX_MODE=observe

Fail-OPEN rule: in observe mode any error (core unreachable, timeout, invalid response) also results in allow — it never blocks. This is the opposite of enforce mode's fail-closed invariant, and it is intentional: observe must never interrupt a Claude Code session.

Use observe mode for dry-run calibration before enabling enforcement: run your normal Claude Code workflows, inspect the JSONL audit log for deny and require_approval records, tune policy as needed, then remove REEFLEX_MODE (or set it to enforce) to activate full fail-closed enforcement.

Metadata

Release files for reeflex-claude 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for reeflex-claude 0.2.0
File Size Uploaded
reeflex_claude-0.2.0.tar.gz 146.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for reeflex-claude 0.2.0
File Interpreter ABI Platform
reeflex_claude-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 234.5 kB

Release files / reeflex_claude-0.2.0.tar.gz

Download URL reeflex_claude-0.2.0.tar.gz
Size 146.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f06605048f68993e007cc52f1c1b6414f65574e168ebbf7853bc52ada2d183ac
BLAKE2b-256 checksum
How to use checksums
056416e71a0d55987d2dc28f7b6b07a4f0d13829a8ebbf5435aa36ca2cd32dcf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / reeflex_claude-0.2.0-py3-none-any.whl

Download URL reeflex_claude-0.2.0-py3-none-any.whl
Size 88.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7e7a15e2919e25c096414e5a6a9caf679eab131e77b001a6aa66464806cb45b5
BLAKE2b-256 checksum
How to use checksums
816458ebc069e2ccaf2c13638f661ee3c86b5f73815e0b63506b0edd9a2c8063
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.2

2 release files

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page