Auto-Approve (LLM)
Remi can route Claude Code permission prompts through a Large Language Model (LLM) and automatically approve, deny, or escalate each request.
The goal is to eliminate notification fatigue for routine approvals (file reads, git status, test runs) while keeping human review for anything ambiguous or dangerous.
Auto-approve is disabled by default and fully opt-in.
How it works
When Claude Code asks for permission to run a tool, a hook fires to the Remi daemon. With auto-approve enabled, the daemon evaluates the request in this order:
- Deny list / deny groups. Any match returns
denyimmediately: no LLM call, no notification. - Allow list / approve groups. Any match returns
approveimmediately: no LLM call, no notification. - Always-escalate tools.
AskUserQuestion,ExitPlanMode, and similar non-binary tools always escalate to you; the LLM never decides them. - The LLM. Otherwise the configured model is called with the tool name and input. It returns one of:
- approve: injects "Yes" into the PTY, Claude continues, no notification.
- deny: injects "No", Claude is blocked, logs the reason.
- escalate: falls through to the normal flow; the prompt reaches your phone or browser.
If the LLM is unreachable, times out, or returns an unparseable response, the request escalates to you. Errors never silently block Claude.
Built-in approve groups
Three read-only/build groups are enabled by default (approve_groups) and are checked before any LLM call:
| Group | Covers |
|---|---|
read-only | The Read, Glob, Grep, NotebookRead tools, and Bash read commands (cat, head, tail, less, grep, rg, wc, ls, jq, and similar) |
vcs-read | git show, git log, gh release list, gh search, gh status |
build-test | bun test, bun run test, bun run typecheck, tsc --noEmit, biome check, pytest, vitest run, and similar |
Group matching is compound-segment-aware, not a plain substring match, so git log && rm -rf / does not slip through as vcs-read.
build-test deliberately excludes eslint, because --rulesdir / --resolve-plugins-relative-to can load and execute arbitrary JavaScript.
Disable a group by removing it from approve_groups, or block it outright by adding it to deny_groups.
The local backend: Yooz Engine
Auto-approve's local backend is the Yooz Engine, a small helper process that remi downloads and supervises itself.
It replaced Ollama in remi 0.7.0; a config carrying provider = "ollama" fails to load and stops every remi command until it is edited.
- The engine listens on remi's reserved loopback port,
http://127.0.0.1:19924. Nothing else on the machine should bind that port. - On first use, remi fetches the helper binary from a pinned yooz-engine release and caches it under
~/.remi/engine/. The pin is deliberate: an engine version is not swapped out from under you by an unrelated remi upgrade. - The default model,
YoozLabs/Qwen3.5-4B-qat-lean-4bit-mlx(about 2.4 GB), is fetched from HuggingFace the first time it is needed. A fresh install therefore downloads several gigabytes before its first evaluation; progress may sit at 0% throughout (a known engine bug), completion is detected from bytes on disk. - Only a verb that actually needs an engine starts one.
remi model statusdeliberately does not, so it can report an engine being down rather than triggering a fetch just to answer the question.remi model useneeds no engine at all: it only writes remi's config.
Platform support
| Platform | Backend | Status |
|---|---|---|
| Apple Silicon macOS | yooz (the engine above, using MLX) | Supported |
| Linux | llamacpp (a bundled llama-server, OpenAI-compatible) | In progress |
| Intel Mac | Neither backend can run | Not supported; remi reports this at boot instead of waiting on an engine that can never appear |
The default provider is chosen by platform automatically (detectLocalLLMPlatform); you do not need to set it by hand on a supported platform.
Other backends
| Provider | Notes |
|---|---|
yooz | The Yooz Engine, local, on Apple Silicon macOS |
llamacpp | Local llama-server, on Linux |
openrouter | Hosted; routes to Claude, GPT, or any model OpenRouter serves |
| Custom URL | Any OpenAI-compatible chat completions endpoint (self-hosted, vLLM, LiteLLM, etc.) |
Quick start
# Enable auto-approve; remi fetches the engine and the default model
# the first time a permission actually needs evaluating.
remi --auto-approve
Use Claude Code normally.
Routine commands covered by the built-in groups (reads, git log, bun test) are approved instantly with no LLM call.
Everything else is evaluated by the model; write operations and anything the model is unsure about escalate to your phone.
Check what is downloaded and which model remi is configured to use:
remi model ls
Managing models: remi model
remi model ls Inventory: size on disk, downloaded, resident
remi model ps Models resident in memory right now
remi model status Engine reachable? which model? download in flight?
remi model pull <id> Download weights (does not change the active model)
remi model cancel <id> Abort an in-flight download
remi model rm <id> Delete weights, reporting disk reclaimed
remi model cleanup Engine's one-shot disk-hygiene sweep
remi model load <id> Load already-downloaded weights
remi model unload <id> Free a model from memory
remi model use <id> Set the default model (persisted in config)
remi model restart Relaunch the engine on the version remi pins
Models are named by their registered HuggingFace repo id, e.g. YoozLabs/Qwen3.5-4B-qat-lean-4bit-mlx.
The engine also accepts its own short id for the same model; either works wherever <id> is expected.
remi model use only writes remi's config; the engine forgets its own preference on restart, so restart any running daemons for a new default to take effect.
remi model restart relaunches the engine itself on the version remi pins, useful after upgrading remi, since an engine that is already answering keeps running on whatever version started it.
CLI flags
--auto-approve Enable auto-approve (overrides config)
--no-auto-approve Disable
--auto-approve-model MODEL e.g. YoozLabs/Qwen3.5-4B-qat-lean-4bit-mlx
--auto-approve-provider PROVIDER yooz | llamacpp | openrouter | custom URL
--auto-approve-api-key KEY Required for OpenRouter and most hosted providers
--auto-approve-allow STR Substring allow pattern (repeatable, appends to config)
--auto-approve-deny STR Substring deny pattern (repeatable, appends to config)
--auto-approve-instructions TEXT Natural-language guidance appended to the LLM prompt
--auto-approve-multichoice MODE skip (default) | evaluate (LLM picks an option index)
--auto-approve-multichoice-model M Alternate model for multi-choice; empty = main model
Config file
Persistent settings go in ~/.remi/config.toml:
[auto_approve]
enabled = true
provider = "yooz" # "yooz" | "llamacpp" | "openrouter" | custom URL; platform-detected default
model = "YoozLabs/Qwen3.5-4B-qat-lean-4bit-mlx"
api_key = "" # Required for OpenRouter
base_url = "http://127.0.0.1:19924" # remi's reserved engine port
timeout = 30 # Seconds; escalates on timeout
log_decisions = true # Log every approve/deny to ~/.remi/remi.log
# User-defined rules. Substring matching.
allow = ["Read", "Glob", "Grep"] # Tool names (non-Bash tools) match by exact name
deny = []
# Built-in groups, checked before allow/deny patterns and before the LLM.
approve_groups = ["read-only", "vcs-read", "build-test"]
deny_groups = []
# Natural-language guidance appended to the LLM's system prompt
instructions = ""
# Background-agent commands worth a heads-up notification even though they
# already ran (see "Subagent permissions" below). Irreversible-only by default.
subagent_alert = [
"rm -rf", "rm -f", "push --force", "push -f ",
"reset --hard", "DROP TABLE", "TRUNCATE", "sudo ", "chmod 777",
]
Advanced settings
These have working defaults; most setups never touch them.
| Key | Default | Purpose |
|---|---|---|
multichoice | "skip" | "evaluate" lets the LLM pick an index on a multi-choice prompt instead of always escalating |
multichoice_model | "" | Alternate model for multi-choice evaluation; empty uses model |
escalate_model | "" | A second-opinion model consulted only when the primary model would escalate (main session only); empty disables it |
escalate_timeout | 0 | Timeout for escalate_model; 0 reuses timeout |
queue_timeout | 240 | Max seconds a permission eval waits in the serialization queue (one LLM call runs at a time) before escalating |
cache_idle | 300 | Seconds of inactivity before the model's prompt cache is dropped (weights stay resident) |
keep_alive | 1800 | Seconds a model stays resident after its last evaluation before being unloaded |
engine | "owned" | "owned": remi starts and supervises its own engine. "shared": another host (e.g. super-yooz) owns the engine on this port; remi reads and evaluates but never spawns, unloads, or deletes models |
engine_path | "" | Path to a helper binary remi should start in "owned" mode; empty means remi still attaches to an engine already on the port, or fetches one automatically |
model_cache | "" | Where the engine downloads weights; empty uses the engine's own default cache |
disable_thinking | true | Reasoning is off by default: on the small models auto-approve targets, an unsuppressed reasoning pass has been observed to consume the whole token budget and return no verdict at all |
always_escalate_tools | ["AskUserQuestion", "ExitPlanMode"] | Tools that always escalate, never auto-decided |
Environment variables
REMI_AUTO_APPROVE=true|false
REMI_AUTO_APPROVE_MODEL=<model>
REMI_AUTO_APPROVE_PROVIDER=<name>
REMI_AUTO_APPROVE_BASE_URL=<url>
REMI_AUTO_APPROVE_API_KEY=<key>
REMI_AUTO_APPROVE_ALLOW=pat1,pat2,... # Comma- or newline-separated
REMI_AUTO_APPROVE_DENY=pat1,pat2,...
REMI_AUTO_APPROVE_INSTRUCTIONS=<text>
Priority: CLI flag > env var > config file > built-in default.
Pattern syntax
Allow and deny lists
Plain substring match. No glob, no regex.
For Bash:
- Pattern
"git push"matches any Bash command containing the stringgit push, including compound commands likecd /foo && git push origin main. - Trailing space disambiguates prefixes: pattern
"sudo "matchessudo rm -rf /but notsudoku. - Pipe patterns match as substrings:
"| bash"catchescurl ... | bash.
For other tools (Read, Write, Edit, Glob, Grep, WebFetch, etc.):
- Pattern is the exact tool name. Pattern
"Read"matches any invocation of the Read tool. - Tool name is not substring-matched against
file_pathorcontent, only the name.
Design note: substring matching on allow/deny is intentionally permissive; it solves Claude Code's compound-command limitation, where Bash(git push:*) fails to match cd /foo && git push.
The deny list is your safety rail: any match short-circuits to deny before the LLM is consulted.
The built-in groups above use compound-segment-aware matching instead, which is safer for the defaults that ship on.
Model requirements
Auto-approve decisions are security-sensitive. Remi ships a test suite of 38 scenarios covering destructive operations, reverse shells, obfuscation, and data exfiltration. Measured against a real engine (2026-07): the default model scores 38/38 with zero unsafe approvals and a p95 latency of 2.26 seconds, against 12.2 seconds for the retired Ollama-era default.
Small models are unsafe for this job. Use a 4B-or-larger model; models under 2B parameters have been observed to approve dangerous commands.
The engine's TouchUp (proofreading) tiers are not a substitute even when they also score 38/38.
One tier's passing score includes six responses that carry no verdict at all, they echo the prompt back, and an unparsable response is treated as escalate, so it "passes" by accident rather than by judgment.
Use the default YoozLabs/Qwen3.5-4B-qat-lean-4bit-mlx or another model you have verified against your own risk tolerance.
Subagent permissions
The PTY, not the hook, decides whether a background subagent's permission reaches you.
A subagent-tagged PermissionRequest is never evaluated at hook time. Claude Code tags hook events from a background subagent or team member with an agent_id field; when the daemon sees one, it parks the request and returns passthrough immediately, without calling the LLM.
At that point the daemon has no way to know whether the prompt will ever render on screen, and most never do: Claude's own permission flow absorbs the majority of them silently.
If the parked prompt does render on the PTY, it is evaluated at that moment: an approve / deny / pick verdict is typed directly into the on-screen prompt, with no card and no interruption, while an escalate verdict still pushes a card to your phone carrying the model's summary, exactly like a main-session escalation.
An "always allow" option is never auto-picked; persisting a permission rule stays your call.
Because most subagent permissions are absorbed silently, subagent_alert (see the config above) exists purely for visibility: a match against an allowlist-covered subagent command fires a dismiss-only notification and an audit log line.
It does not gate anything; the command already ran.
Defaults are irreversible-only commands (rm -rf, push --force, reset --hard, DROP TABLE, sudo , and similar).
Broader patterns like curl or ssh are opt-in per machine, since on a session driving many agents they fire on benign traffic often enough that the banner stops being useful.
If you run multiple Claude Code sessions in different directories, each gets its own Remi daemon and its own auto-approve; they are independent.
Logs
Every decision logs to ~/.remi/remi.log:
[AutoApprove abc12345] Bash: approve (1842ms) - git log is a routine read
[AutoApprove abc12345] Injected "1" into PTY (approved)
[AutoApprove abc12345] DENIED Bash: deny-matched pattern: "rm -rf /"
[AutoApprove abc12345] ERROR Bash: LLM timeout after 30001ms
The 8-character tag is the Remi session ID prefix. Grep by tag to isolate one session's activity when multiple daemons are running.
Troubleshooting
Auto-approve escalates everything: check the engine is reachable and the model is present with remi model status.
If the engine has never been started, the first evaluation triggers a fetch of both the helper and the model, which can take a while on a slow connection.
Auto-approve approves too much: switch to a stricter model, add items to your deny list or deny_groups, or provide tighter instructions.
Latency is too high: check remi model ps to confirm the model is actually resident (a cold load adds real latency to the first call after cache_idle/keep_alive unloads it).
You can also try a smaller model at the cost of safety; see Model requirements.
A config with provider = "ollama": this was removed in remi 0.7.0 and now fails to load, blocking every remi command.
Change it to "yooz" on Apple Silicon or "llamacpp" on Linux, and set model to an id the chosen backend serves.
Privacy
- Yooz Engine / llamacpp (recommended): all evaluation happens on your machine. Zero network calls, other than the one-time model download from HuggingFace.
- OpenRouter or other hosted providers: the tool name and input are sent to the provider. Reconsider for sensitive commands.
The evaluation prompt contains the tool name and raw tool_input (e.g. the full Bash command or file path).
It does NOT contain your Claude Code conversation, code contents, or Remi daemon state.