Skip to Content
ConceptsAutonomy Levels

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.

LayerWhat it isWhere it lives
Runtime enumThree states the approval gate actually branches on at execution time[autonomy].level in config + AutonomyLevel enum in code
Manual / Smart / Strict / Off presetsFour 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:

ValueWhat it does
readonlyOnly read-shaped tools auto-approve. Writes and shell prompt for approval.
supervisedDefault. Tools listed in [autonomy].auto_approve auto-approve; everything else prompts.
fullSkips 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")] in src/security/policy.rs), so the config value is readonlynot read_only. Writing read_only errors with “unknown variant”.

Two important properties of the runtime enum:

  • Forbidden paths are non-negotiable across all three states. Even full cannot bypass them — that’s enforced in code (policy.rs), not policy.
  • block_high_risk_commands defaults to false (since v0.6.89 — usable-by-default for local capability). High-risk commands like rm, sudo, curl, wget, ssh, chmod, mkfs, iptables, mount are therefore not pre-blocked; they still pass the command allowlist and the approval gate. Set the flag to true to add a hard block that even full autonomy 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 = 500

Defaults:

  • auto_approve = ["file_read", "memory_recall"] — exactly two tools auto-approve by default.
  • max_actions_per_hour = 200 (raised from 20 in 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_paths includes /home, /tmp, /var, ~/.config, ~/.aws, etc. — only workspace_dir is reachable when workspace_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:

RiskExamples
Highrm, sudo, curl, wget, ssh, chmod, mkfs, iptables, mount
Mediumgit commit/push/reset/..., npm install, cargo add, touch, mv, cp
LowEverything 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/:

PresetWhat 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 L1L4 labels from earlier releases are legacy aliases onlyrantaiclaw autonomy l1 still resolves to manual, l2smart, l3strict, l4off — 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_line prompt. The prompt blocks until the user types y/yes/approve or n/no/deny. There is no timeout on this prompt todaytimeout_secs is 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, or ok; a bare /deny or no is 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 exposes POST /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 command field — the redact() helper exists in src/security/mod.rs but is not wired into audit serialization. HMAC signing of audit events is also not implemented (the sign_events config 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_bind requirement (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.rsAutonomyLevel enum, command parser, risk classifier, forbidden_path enforcement
  • src/approval/mod.rs — CLI prompt, session-scoped “Always” allowlist
  • src/approval/policy_writer.rsPolicyPreset (Manual/Smart/Strict/Off) + emits the preset files; accepts l1l4 as legacy aliases
  • src/approval/presets/policy_{manual,smart,strict,off}.toml — the actual preset bundles
  • src/security/audit.rs — append-only audit log writer + rotation
  • src/config/schema.rs[autonomy] config fields and defaults
Last updated on