Skip to content

Approvals & permissions

An agent that wants to run something it is not cleared for stops mid-turn and asks. Nothing runs while it waits. You answer in the chat it asked in, in the dashboard, or from the terminal — the three are one queue.

An approval card

Answering

In a chat:

/approve allow this one
/approve always …and stop asking for this exact command
/deny refuse it

In nouride chat, the prompt appears inline and a keypress answers it. From the CLI anywhere:

Terminal window
nouride approvals # what is waiting
nouride approvals allow-once <id>
nouride approvals allow-always <id>
nouride approvals deny <id>

An unanswered approval times out. That is deliberate — a turn cannot hang forever waiting for somebody who has gone to bed.

How much gets asked at all

Before [security.exec] is consulted, one setting decides how much this agent asks this person. Three levels:

normalAsk whatever [security.exec] says
risky (default)Ordinary calls run unprompted. The hardline classes below still ask
noneNothing asks, including the hardline classes

risky is the default because a fresh install is one person on their own machine, and being asked about every ls is what gets a gate switched off wholesale.

Three places can set it, and the first one that states a level wins:

  1. The agent[agent] approvals in its pack. An agent is a shared surface: several people talk to the same one, so a posture that changed depending on who spoke would be a posture nobody could reason about. per-person is how an agent declines to decide, and it is the default.
  2. The person/approvals risky or /approvals normal in a chat. It follows the sender, not the conversation, so it survives /new, /reset and moving to another channel.
  3. The daemon[security] approvals.

/approvals with no argument shows the level and says which layer decided, because “why am I being asked” and “why am I not” are the same question.

The three exec knobs

Command execution is governed by three more settings under [security.exec]. They apply when the level above is normal, and they are what a standing grant is recorded against.

mode — the ceiling.

denyexec is unavailable, full stop
allowlistOnly programs in allowlist, or with a standing grant, may run
fullAnything may run, subject to ask

ask — when a human is consulted.

offNever
on-missThe first time each command is seen; the answer is remembered
alwaysEvery single call, and nothing is ever remembered

ask_fallback — what applies when nobody can be reached. No gateway connected, nobody watching. Either deny (refuse anything that would have asked) or allowlist. There is deliberately no full: an unreachable operator must never read as consent.

The default pair is mode = "full", ask = "on-miss": the agent may run anything, and each new command asks once. Note that at the default risky level most calls never reach that question — normal is what turns it on.

Standing grants

allow-always creates a grant — a remembered yes for one exact command. Review and revoke them:

Terminal window
nouride grants # what has a standing yes
nouride grants revoke <id>
nouride grants revoke --all

Or /grants in a chat.

What can never become a grant

Some classes ask every time, regardless of ask, and can never become a standing permission. “Stop asking me” is a sentence about friction — the fiftieth read of a root-owned log — and reading it as consent for rm -rf is not what anybody means by it.

ClassExamples
destructiverecursive deletes, mkfs, overwriting a device
system-controlreboot, shutdown, taking a service down, pct restart
infrastructureterraform apply, terraform destroy, kubectl delete, helm uninstall, ansible-playbook
credentialsreading or writing credential paths
runtime-selfthis daemon’s own state

Their read halves are not gated at allterraform plan, kubectl get, describe, and anything with --dry-run or --check. A gate on reading is a gate people learn to click through, and an operator who dismisses prompts by reflex is the actual failure mode.

Privilege escalation is looked through rather than treated as its own class: sudo rm -rf / classifies as destructive, so relaxing the quiet half of sudo cannot smuggle the loud half past you.

Checking before it happens

Terminal window
nouride policy check rm -rf /tmp/build

Answers “would this be gated, and why” without running anything — and it works while the daemon is down, which is exactly when somebody is debugging a refusal.

Terminal window
nouride policy # what always asks, and what never does

The always-allowed list

[security.exec] allowlist names programs that skip the ask entirely, by basename. The arguments are not consulted, which is what makes the default list short:

allowlist = [
"ls", "pwd", "cat", "head", "tail", "wc", "date", "echo", "which", "file", "stat",
"grep", "rg", "tree", "df", "du", "uname", "whoami", "basename", "dirname",
]

Notably not safe to add:

env, printenvprint the process environment, which holds every provider key
find, xargs-delete and -exec make them arbitrary-mutation tools
sed, awk-i rewrites in place, system() shells out
gitstatus is safe, push --force is not, and a basename cannot tell them apart

blocked is the other direction — your own veto list, refused before anything else. It beats a standing grant and it beats mode = "full".

Per-tool gating

exec is not the only thing that can be gated. Any tool can be, per agent, in .nouride/agents/<id>/config.toml:

[tools]
enabled = ["read", "web_search", "web_fetch", "memory_read", "memory_write", "send_file"]
approval_required = ["exec", "write", "edit"]
blocked = ["rm", "shutdown", "reboot"]

A tool nobody listed in approval_required runs without asking. That includes MCP tools — to gate one, name it by its qualified name, "nouva-mcp__memory_query", the same name the model calls it by.

Secrets an agent may spend but never read

An agent can use a credential it has never seen. exec_secrets names secrets whose values arrive in a child process as environment variables at the moment the command runs:

[security]
exec_secrets = ["twit_auth_token"] # arrives as $TWIT_AUTH_TOKEN

The model never sees the value, so nothing lands in the prompt, the tool result or the transcript. It is opt-in per agent, because “which credentials may this agent spend” is worth answering deliberately.

Files that cannot be an environment variable — a service-account JSON, an OAuth cache that gets rewritten on refresh — go in the credentials directory instead.

When nobody is watching

A scheduled job that hits an approval is governed by [cron] unattended_approval:

  • auto (default) — approve it, and record the decision in the audit log against the job. A 3am job that stops at a prompt nobody will ever see has not run at all.
  • deny — refuse it with a reason the agent can report, so the job fails honestly instead of quietly doing less than it was asked.

Either way the hardline classes above still ask the target chat, and still fail closed when nobody answers.

The audit log

Every tool execution, approval and denial is appended to SQLite.

Terminal window
nouride audit # recent audited actions
nouride audit 100 # more of them

The dashboard shows the same trail under System Logs. How long it is kept is [retention] audit_days, 30 by default — worth knowing, because a few other features derive from it, including “which skills has nobody read”.