Skip to main content

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:

  1. Deny list / deny groups. Any match returns deny immediately: no LLM call, no notification.
  2. Allow list / approve groups. Any match returns approve immediately: no LLM call, no notification.
  3. Always-escalate tools. AskUserQuestion, ExitPlanMode, and similar non-binary tools always escalate to you; the LLM never decides them.
  4. 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:

GroupCovers
read-onlyThe Read, Glob, Grep, NotebookRead tools, and Bash read commands (cat, head, tail, less, grep, rg, wc, ls, jq, and similar)
vcs-readgit show, git log, gh release list, gh search, gh status
build-testbun 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 status deliberately does not, so it can report an engine being down rather than triggering a fetch just to answer the question. remi model use needs no engine at all: it only writes remi's config.

Platform support

PlatformBackendStatus
Apple Silicon macOSyooz (the engine above, using MLX)Supported
Linuxllamacpp (a bundled llama-server, OpenAI-compatible)In progress
Intel MacNeither backend can runNot 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

ProviderNotes
yoozThe Yooz Engine, local, on Apple Silicon macOS
llamacppLocal llama-server, on Linux
openrouterHosted; routes to Claude, GPT, or any model OpenRouter serves
Custom URLAny 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.

KeyDefaultPurpose
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_timeout0Timeout for escalate_model; 0 reuses timeout
queue_timeout240Max seconds a permission eval waits in the serialization queue (one LLM call runs at a time) before escalating
cache_idle300Seconds of inactivity before the model's prompt cache is dropped (weights stay resident)
keep_alive1800Seconds 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_thinkingtrueReasoning 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 string git push, including compound commands like cd /foo && git push origin main.
  • Trailing space disambiguates prefixes: pattern "sudo " matches sudo rm -rf / but not sudoku.
  • Pipe patterns match as substrings: "| bash" catches curl ... | 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_path or content, 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.