Skip to main content

Reeflex Claude Code adapter: deterministic PreToolUse governance hook (allow/deny/require_approval), fail-closed by construction.

Project description

reeflex-claude

Reference adapter: Claude Code PreToolUse hook for Reeflex governance.

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

Install / wire up

1. Install.

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/merges the PreToolUse hook entry, fail-CLOSED by default
reeflex-claude check   # verifies: deny scenario -> hook exits correctly

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.

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: Bash|Write|Edit|MultiEdit|Read|Glob|Grep|LS|NotebookEdit|WebFetch|WebSearch
[reeflex-claude]   command: 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 this structurally fixes the old fail-open bug: once installed via pip, reeflex-claude hook is a console entry point resolved from PATH — there is no absolute path to get wrong and no cwd-dependent python -m import that fails with No module named reeflex_claude when Claude Code runs the hook from the project's working directory instead of reeflex-claude/. See "Development install" below for the git-clone path, where that failure mode remains possible if the hook command is misconfigured.

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": "Bash|Write|Edit|MultiEdit|Read|Glob|Grep|LS|NotebookEdit|WebFetch|WebSearch",
        "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
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 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, staging cert (set verify_ssl=false), rate-limited, may reset anytime; not for production.

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

--verify-ssl false is only needed because the staging endpoint uses a self-signed/untrusted certificate; never set it against a production core. 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 staging endpoint responds.

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

export REEFLEX_CORE_URL=https://api-dev.reeflex.io
export REEFLEX_VERIFY_SSL=false             # staging cert is not a publicly-trusted CA cert
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.

Limits / upgrade paths

  • Bash classification is heuristic (regex on the command string). A full parse tree would be more accurate. UPGRADE: replace _bash_verb with a shell-AST parser once tooling stabilises.
  • Stub signing: meta.signature = "ed25519:stub:...". UPGRADE: Vault-backed ed25519 signing once the key management path is implemented (SPEC §6 note).
  • REEFLEX_CLAUDE_STRICT: unset by default so coding agents are not blocked on every npm install. 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reeflex_claude-0.1.6.tar.gz (58.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

reeflex_claude-0.1.6-py3-none-any.whl (36.3 kB view details)

Uploaded Python 3

File details

Details for the file reeflex_claude-0.1.6.tar.gz.

File metadata

  • Download URL: reeflex_claude-0.1.6.tar.gz
  • Upload date:
  • Size: 58.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for reeflex_claude-0.1.6.tar.gz
Algorithm Hash digest
SHA256 5fa8295d911d3c9768a41f72f43643779aa40f831cbafe625b31ac288f4ddf28
MD5 41c36be35b82a1b4aa79cf22c0309100
BLAKE2b-256 d3130f744e4255abe048772e9d4bcf6668ae6a5ff5d739dc1441461290b07f96

See more details on using hashes here.

File details

Details for the file reeflex_claude-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: reeflex_claude-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 36.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for reeflex_claude-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 c30ac6174d20f3f3da1ff32eff0f85b5f48b0ea23bb556c5a349a57c4a5a586c
MD5 80eb397d12ca850c29474fcbde64b8c0
BLAKE2b-256 c84b24312b7241950a261ce837abf256da05797b93acc6b110c49818f1a038b9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page