Autonomy Levels
Autonomy in RantAIClaw is two layers that the docs sometimes conflate. Knowing the difference will save you a lot of confusion when you read config files or audit logs.
| Layer | What it is | Where it lives |
|---|---|---|
| Runtime enum | Three states the approval gate actually branches on at execution time | [autonomy].level in config + AutonomyLevel enum in code |
| Manual / Smart / Strict / Off presets | Four named bundles of autonomy + command allowlist + forbidden paths that rantaiclaw autonomy <preset> (and the onboarding wizard) writes to disk | <profile>/policy/autonomy.toml + command_allowlist.toml + forbidden_paths.toml |
The presets write values into the runtime enum (and other files) when you pick one. After that, the runtime only sees the enum + lists.
The runtime layer — three states
AutonomyLevel (src/security/policy.rs) has three values. Serde-lowercased:
| Value | What it does |
|---|---|
readonly | Only read-shaped tools auto-approve. Writes and shell prompt for approval. |
supervised | Default. Tools listed in [autonomy].auto_approve auto-approve; everything else prompts. |
full | Skips auto-approve list; runs tools without prompting — but still respects forbidden paths, plus block_high_risk_commands if you have turned it on (it defaults to false). |
The enum serializes lowercase (
#[serde(rename_all = "lowercase")]insrc/security/policy.rs), so the config value isreadonly— notread_only. Writingread_onlyerrors with “unknown variant”.
Two important properties of the runtime enum:
- Forbidden paths are non-negotiable across all three states. Even
fullcannot bypass them — that’s enforced in code (policy.rs), not policy. block_high_risk_commandsdefaults tofalse(since v0.6.89 — usable-by-default for local capability). High-risk commands likerm,sudo,curl,wget,ssh,chmod,mkfs,iptables,mountare therefore not pre-blocked; they still pass the command allowlist and the approval gate. Set the flag totrueto add a hard block that evenfullautonomy cannot bypass.
Per-tool gating
Inside [autonomy], two list keys decide which named tools auto-approve or always prompt:
[autonomy]
level = "supervised"
auto_approve = ["file_read", "memory_recall"]
always_ask = []
workspace_only = true
max_actions_per_hour = 200
max_cost_per_day_cents = 500Defaults:
auto_approve = ["file_read", "memory_recall"]— exactly two tools auto-approve by default.max_actions_per_hour = 200(raised from20in v0.6.83 — the old budget was exhausted mid-turn)max_cost_per_day_cents = 500(tracked for reporting; not a hard stop in the agent loop)forbidden_pathsincludes/home,/tmp,/var,~/.config,~/.aws, etc. — onlyworkspace_diris reachable whenworkspace_only = true.
There is no [autonomy.overrides] block with glob keys. Per-command shape rules live in a separate file documented below.
Command allowlist (separate file)
For shell specifically, command-shape decisions are driven by <profile>/policy/command_allowlist.toml:
[command_allowlist]
patterns = [
"git status",
"git log *",
"ls *",
"cat *",
"cargo test *",
]Patterns are quote-aware globs over the command string. A request to run cargo test --lib matches "cargo test *" and auto-approves; a request to run cargo publish does not match and prompts (or denies under readonly).
The runtime parses commands with defense-in-depth: subshells (`, $(, ${, <(, >(), output redirects (>, >>), tee, single-&, find -exec/-ok, git config/alias/-c, and env-var prefixes (FOO=bar cmd) are all blocked or stripped before allowlist matching.
Risk classification
Independently of allowlists, every shell invocation is classified:
| Risk | Examples |
|---|---|
| High | rm, sudo, curl, wget, ssh, chmod, mkfs, iptables, mount |
| Medium | git commit/push/reset/..., npm install, cargo add, touch, mv, cp |
| Low | Everything else |
block_high_risk_commands defaults to false, so high-risk commands are classified but not pre-blocked — they still go through the command allowlist and the approval gate. Set block_high_risk_commands = true to deny them outright, even under full autonomy.
The presets — Manual, Smart, Strict, Off
Presets are named bundles of autonomy level + command allowlist + forbidden paths. Apply one from the CLI at any time:
rantaiclaw autonomy # print the active preset + list all four
rantaiclaw autonomy smart # switch to the recommended default
rantaiclaw autonomy off # disable gating (CI / fully-trusted only)rantaiclaw setup / rantaiclaw onboard offer the same four during first-run. Picking one writes a bundle of files (policy/autonomy.toml + command_allowlist.toml + forbidden_paths.toml) to <profile>/policy/:
| Preset | What it writes |
|---|---|
Manual (manual) | mode = "manual", empty allowlist (every command prompts), level = "supervised", wide forbidden_paths, timeout_secs = 60 |
Smart (smart) | mode = "manual", level = "supervised", read-only + trivially-safe command patterns pre-allowed (ls, cat *, git status, grep *, web_search *, file_read *, memory_*, etc.), timeout_secs = 60 |
Strict (strict) | mode = "strict", deny-by-default (no prompt fallback), Smart read set + safe-write seeds (memory_write *, skill_install *, cron_*, session_*), level = "supervised", timeout_secs = 0 |
Off (off) | mode = "off", gating disabled, minimal forbidden floor (the rantaiclaw secrets dir stays sealed), runtime level = "full" |
The L1–L4 labels from earlier releases are legacy aliases only — rantaiclaw autonomy l1 still resolves to manual, l2→smart, l3→strict, l4→off — but the preset ids are now the words. full is also accepted as an alias for off.
Important: preset names are not runtime states. They write configuration; once the runtime is loaded, it only sees the three-value enum + the list-shaped policy files. If you hand-edit [autonomy].level after picking a preset, the runtime behavior changes but the preset name on disk no longer reflects what’s running.
Hot-reload — no restart for most changes
Since v0.6.87, switching the preset (rantaiclaw autonomy off/smart/…) or editing permissions takes effect on running channels at the next message — no daemon restart. The runtime shares the autonomy level via an interior Arc<RwLock> and re-applies it on each config.toml change, alongside the command allowlist, approval owners (approval_owners), and the guest capability gate (guest_allowed_tools / guest_allowed_commands).
Two things still apply only at boot, by design — they narrow the security boundary, so they are read once at startup:
forbidden_paths- the medium/high-risk approval flags (e.g.
require_approval_for_medium_risk)
Removing a command from an allowlist likewise takes full effect on restart; the live sync only widens.
What approval prompts look like in practice
Approval flow depends on the originating channel:
- CLI channel — interactive
stdin.read_lineprompt. The prompt blocks until the user typesy/yes/approveorn/no/deny. There is no timeout on this prompt today —timeout_secsis written to the policy file but no consumer reads it. If the user never responds, the tool call hangs. - Non-CLI channels (Discord, Telegram, Slack, …) — a pending Supervised-mode request is relayed into the chat and resolved from there (v0.6.84). An approval owner (
[channels_config] approval_owners) can reply/approve,approve,yes,y, orok; a bare/denyornois honored from anyone. With several requests pending, the bot lists them and asks you to pick one. If no owner responds before the deadline the request auto-denies (default 5 minutes). The gateway also exposesPOST /api/v1/approvals/{id}for programmatic resolution.
Tools in auto_approve (or, for shell, commands matching the command_allowlist) skip the prompt entirely on every channel.
The audit log
Every approval decision goes to <profile>/audit.log (JSONL, append-only). Schema:
{
"timestamp": "2026-05-08T13:42:11.337Z",
"event_id": "<uuid-v4>",
"event_type": "command_execution",
"actor": { "channel": "cli", "user_id": "...", "username": "..." },
"action": { "command": "cargo test --lib", "risk_level": "low", "approved": true, "allowed": true },
"result": { "success": true, "exit_code": 0, "duration_ms": 4421, "error": null },
"security": { "policy_violation": false, "rate_limit_remaining": 199, "sandbox_backend": "landlock" }
}Event types: command_execution, file_access, config_change, auth_success, auth_failure, policy_violation, security_event.
The log rotates when it reaches max_size_mb (default 100), creating audit.log.1.log, .2.log, … up to .10.log.
Caveat: the audit serializer does not currently apply secret redaction to the
commandfield — theredact()helper exists insrc/security/mod.rsbut is not wired into audit serialization. HMAC signing of audit events is also not implemented (thesign_eventsconfig flag is unused).
What is not relaxed by autonomy
Some boundaries are unconditional and not affected by which level / preset is active:
- forbidden paths
- TLS verification on outbound calls
- gateway’s
allow_public_bindrequirement (localhost bind, pairing, rate limits) - channel allowlists (per-channel
allowed_*keys — see Channels)
block_high_risk_commands is an opt-in hard stop (default false): once enabled, no autonomy level bypasses it — but it is not on by default, because local capability is usable-by-default. Relaxing an exposure boundary requires editing its specific config field. The autonomy knob is not a backdoor.
Reading the code
src/security/policy.rs—AutonomyLevelenum, command parser, risk classifier, forbidden_path enforcementsrc/approval/mod.rs— CLI prompt, session-scoped “Always” allowlistsrc/approval/policy_writer.rs—PolicyPreset(Manual/Smart/Strict/Off) + emits the preset files; acceptsl1–l4as legacy aliasessrc/approval/presets/policy_{manual,smart,strict,off}.toml— the actual preset bundlessrc/security/audit.rs— append-only audit log writer + rotationsrc/config/schema.rs—[autonomy]config fields and defaults