Reflex

Claude Code hooks for production safety

Reflex installs as a Claude Code plugin or through its setup command and adds a PreToolUse hook that checks every Bash command before it runs. Its deterministic rules block destructive production operations and force pushes to main, and it knows the AWS profile, kube context, Terraform workspace and git branch a command runs in.

Install: npx @ursuciprian/reflex setup starts with local rules in shadow mode, no account or key. Plugins for Claude Code, Codex CLI and opencode: setup guide.

How do I stop Claude Code from running dangerous commands?

Install Reflex, which adds a Claude Code PreToolUse hook that checks every Bash command before it runs: npx @ursuciprian/reflex setup. Its deterministic rules deny rm -rf ~, destructive operations on production and force pushes to main, and ask before reads of SSH private keys, ~/.aws/credentials, .netrc, .pgpass, .env files or Kubernetes secrets; the rules block in shadow mode too. Commands the rules do not cover are asked about (local engine) or judged by Jev in context (AWS profile, kube context, Terraform workspace, git branch, the script the command runs). After a shadow period, reflex setup --mode enforce puts those judgments in front of the agent.

See: docs/SETUP.md, real-world scenarios with outputs.

How do I install Reflex as a Claude Code plugin?

Add the marketplace in this repository and install the plugin, inside Claude Code: /plugin marketplace add ursuciprian/reflex, then /plugin install reflex@reflex (or, from a shell, claude plugin marketplace add ursuciprian/reflex and claude plugin install reflex@reflex). The plugin wires the same Claude Code hooks as reflex setup: the PreToolUse command gate on Bash and subagent spawns, the post-tool records, conditional instructions and the prompt injection guard. It adds read-only commands (/reflex:status, /reflex:check <command>, /reflex:report, /reflex:replay, /reflex:queue, /reflex:suggest). It needs Node.js 18+ on the PATH and no build step, npm install or API key; with no saved settings it runs the local engine in shadow mode. If reflex setup hooks are also in ~/.claude/settings.json, the plugin's hooks stand down so nothing is judged twice, and reflex doctor shows which one is active. Use reflex setup for other agents, the autonomous profile, or the permission rules that guard Reflex's own files.

See: README: Claude Code plugin, docs/SETUP.md: Claude Code plugin.

Claude Code plugin

This repository is a Claude Code plugin marketplace (.claude-plugin/marketplace.json) that lists one plugin, reflex, in plugin/. Claude Code clones the repository, copies plugin/ into its plugin cache and runs the hooks from there with node: no node_modules, no npx at hook time.

/plugin marketplace add ursuciprian/reflex        # inside Claude Code
/plugin install reflex@reflex
claude plugin marketplace add ursuciprian/reflex  # or from your shell
claude plugin install reflex@reflex
claude plugin update reflex@reflex                 # later, for a new release

Then restart the session or run /reload-plugins. To try it for one session without installing, clone the repository and run claude --plugin-dir /path/to/reflex/plugin.

The plugin bundle (plugin/)

plugin/ is generated from the repository by node scripts/build-plugin.mjs and committed, since the marketplace installs from git. It holds only what plugin mode runs: the runtime modules, the setup files the gate and the guard read (no golden sets, fixtures or plan fixtures), hooks/, commands/, the scripts the commands run (scripts/*.sh), skills/, .mcp.json, .claude-plugin/plugin.json (with userConfig, no icon), README.md and LICENSE. Code that only reflex setup and the selfchecks run is marked in the source with // @reflex:setup-only begin and // @reflex:setup-only end (in Markdown, <!-- @reflex:setup-only begin -->) and left out: the allow answer and everything that produces it, the Hermes adapter, the rewritten tool result, the Keychain and key variable reads, Laya setup, the doctor probes, the shell shim, the System 2 cli backend and every other start of an agent CLI, the selfchecks, test doubles, evals and benchmarks. A region may only hold code plugin mode cannot reach, never a check that makes a decision stricter.

The build fails when a region is unbalanced, when a module does not parse or does not link, when a file is over 256 KiB, when a file other than the icon is binary, or when a forbidden pattern (the list is FORBIDDEN in the script: an allow answer, a rewritten input or output, a Keychain call, a provider key variable, pip, a Hugging Face download, curl, npx, a global npm install, a flag or setting that turns an agent's prompts or hooks off (dangerously-skip-permissions, bypassPermissions, approval_policy, ask-for-approval, full-auto, --yolo, disableAllHooks, permission-mode), the shell shim, and a child process other than node, git, sed or the infra binaries infra.mjs finds) is left in it. After changing anything the plugin ships, rebuild and commit plugin/:

node scripts/build-plugin.mjs                        # writes plugin/ and lists every file with its size
CLAUDE_PLUGIN_ROOT=plugin node test.mjs --plugin-only # the plugin-mode checks against the bundle
claude plugin validate --strict plugin

CI builds it again and fails when plugin/ differs from what is committed. The directory submission to Anthropic uses the plugin path plugin in this repository (the marketplace entry's "source": "./plugin"); resubmit after a release that changes it.

What the plugin adds:

PartWhat it is
hooks/hooks.jsonthe same Claude Code events, matchers and timeouts as install.mjs --agent claude (see step 3): PreToolUse on Bash|Task|Agent (10 s), PostToolUse, PostToolUseFailure and PermissionDenied records (5 s), PermissionRequest (5 s), UserPromptSubmit instructions (10 s) and prompt guard (5 s), and the injection guard on PostToolUse for ^(WebFetch|WebSearch|Read|Bash)$|^mcp__ (15 s). Each runs node "${CLAUDE_PLUGIN_ROOT}/<script>.mjs" <flag> --plugin; a test keeps the file in step with install.mjs
commands//reflex:status, /reflex:check <command>, /reflex:report, /reflex:replay, /reflex:queue, /reflex:suggest. All read-only: Claude runs the plugin's own script with the Bash tool (node "${CLAUDE_PLUGIN_ROOT}/<script>.mjs" --plugin ..., never a reflex from PATH), through the gate like any other command, and never with --write, approve or --push
skills/reflextells Claude when to check a command and replay past sessions, and not to work around a deny
.mcp.jsonthe Reflex MCP server (node "${CLAUDE_PLUGIN_ROOT}/mcp.mjs", the plugin options in its env): read-only, advisory tools reflex_check, reflex_scan, reflex_status, reflex_audit and reflex_explain (MCP server). A local server: it runs in Claude Code and Cowork, not in claude.ai chat

The plugin puts nothing on the Bash PATH (the CLI lives in scripts/, not a top-level bin/, which claude.ai and Cowork refuse to install), and nothing it runs downloads or installs anything: the Laya setup, install.sh and npx are for reflex setup only.

Plugin options

Claude Code asks for these when you enable the plugin; change them later with /plugin, then reflex, then Configure, or under pluginConfigs in settings.json. Each one may stay empty.

OptionValuesEmpty means
enginelocal (rules only, no key, nothing leaves the machine) or jevengine in config.json, else jev when the Jev API key is set, else local
providertypesafe, openrouter, cloudflare, vercel or compatibleprovider in config.json, else typesafe
jev_api_keythe key for that provider (sensitive: kept in the system's secure storage)no Jev: the engine stays local
modeoff, shadow or enforcemode in config.json, else shadow
judge_api_keythe key for a System 2 judge with backend anthropic or openai-compatible (sensitive)no key for System 2

Claude Code hands the options to the hooks as CLAUDE_PLUGIN_OPTION_<KEY> and to the MCP server through its env in .mcp.json. In the plugin those options and Reflex's own config files (~/.config/reflex/config.json, fastlane.json, the policy under ~/.config/reflex/tool-gate/) are the only settings the hooks and the MCP server read. They never ask the macOS Keychain, never read TYPESAFE_API_KEY or another provider's key variable, ANTHROPIC_API_KEY or the judge.key_env variable (every *_API_KEY and *_API_TOKEN variable is removed from their environment, and from every process they start), and ignore every REFLEX_* variable but REFLEX_DATA_DIR (where the logs go), every JEV_* variable and CLOUDFLARE_ACCOUNT_ID (set cloudflare_account_id or provider_url in config.json instead). The Codex CLI plugin below keeps reading the environment and the Keychain as reflex setup does.

The /reflex:* commands run through the Bash tool, which Claude Code gives no plugin options, so they read config.json alone: /reflex:check judges with its engine (local when none is set), and /reflex:status shows its mode and engine and says that the hooks apply the options on top.

Three more differences from reflex setup, all from the plugin directory's policy:

With no options and no saved settings the gate runs the local engine in shadow mode, which is also where a fresh reflex setup starts. The plugin writes no settings. Logs go to ~/.local/state/reflex as usual.

Hooks run with the PATH Claude Code was started with. If node is not on it (for example Claude Code started from a desktop launcher with a minimal PATH), the hook command fails, Claude Code treats that as a non-blocking hook error and the command runs ungated. reflex doctor from the same environment, or a denied /reflex:check git push --force origin main, confirms the gate is live.

Plugin and reflex setup together. Both install the same hooks. When reflex setup (or install.mjs --agent claude) has written Reflex hooks into ~/.claude/settings.json, every plugin hook sees them and exits at once without reading its input or writing a log line, so each call is judged and counted once, by the settings hooks. The plugin checks the user settings file Claude Code reads: $CLAUDE_CONFIG_DIR/settings.json when that variable is set. install.mjs always writes ~/.claude/settings.json, so with CLAUDE_CONFIG_DIR set the setup hooks do not run in Claude Code and the plugin stays active. A settings hook whose script no longer exists (a deleted checkout) does not count: it gates nothing, so the plugin keeps running, and reflex status reports it as an error. reflex status and reflex doctor print which path is active (Claude Code hooks: ...); doctor also probes the plugin's own gate when the plugin is the active one. To switch to the plugin only, remove the Claude Code hooks with the copy that installed them: node ~/.local/share/reflex/lib/node_modules/@ursuciprian/reflex/install.mjs --agent claude --uninstall after reflex setup, or node <checkout>/install.mjs --agent claude --uninstall for a clone (reflex uninstall removes the hooks of every agent and the package). To switch to setup only, claude plugin uninstall reflex@reflex.

What only reflex setup does, because a plugin cannot change settings:

To remove the plugin: claude plugin uninstall reflex@reflex, and claude plugin marketplace remove reflex for the marketplace. Your settings and logs stay, as with reflex uninstall.

Supported agents: Claude Code hooks, Codex hooks and more

AgentHookHow ask is shown
Claude CodePreToolUseClaude Code permission prompt
Codex CLIPreToolUseBlocked; human uses reflex run in their own terminal
pi, oh-my-piextension tool_callNative confirm dialog
opencodeplugin tool.execute.beforeBlocked; human uses reflex run in their own terminal
Hermespre_tool_callHermes approval prompt
Otherscripts/reflex-sh as the shelly/N on the terminal

The injection guard reads tool results and prompts through each agent's own hooks, so what it can do differs:

AgentTool resultsPrompts with a pasted credential
Claude CodePostToolUse on web, MCP, Read and Bash: warn adds a note, block rewrites the resultUserPromptSubmit: blocked, reason shown
Codex CLIPostToolUse on Bash and MCP tools: warn adds a note, block replaces the result with the reason and the cleaned text; web search is not hookableUserPromptSubmit: blocked, reason shown
pi, oh-my-piextension tool_result: block rewrites the result, warn appends a noteextension input: dropped with a notification
opencodeplugin tool.execute.after: block rewrites the result, warn appends a notechat.message: stopped with an error
Hermespost_tool_call is observe-only: logged, and the note reaches the model on the next turncannot block; the model is told not to repeat it
Othernot coverednot covered

Coverage is agent shell tools, subagent spawns (for dedup) and the optional router's own calls. File-edit tools and MCP tool calls go through each agent's own permissions. Hosts without hooks (Claude Desktop, Cursor) get the MCP server, which advises and does not enforce. Doctor cannot prove host trust or verify a native approval dialog; run a harmless command in a fresh agent session and check reflex status. Plain chat confirmation does not unblock a Codex or opencode hook. See docs/SETUP.md for manual steps and limits.