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.
Answering
In a chat:
/approve allow this one/approve always …and stop asking for this exact command/deny refuse itIn nouride chat, the prompt appears inline and a keypress answers it. From the CLI anywhere:
nouride approvals # what is waitingnouride 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:
normal | Ask whatever [security.exec] says |
risky (default) | Ordinary calls run unprompted. The hardline classes below still ask |
none | Nothing 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:
- The agent —
[agent] approvalsin 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-personis how an agent declines to decide, and it is the default. - The person —
/approvals riskyor/approvals normalin a chat. It follows the sender, not the conversation, so it survives/new,/resetand moving to another channel. - 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.
deny | exec is unavailable, full stop |
allowlist | Only programs in allowlist, or with a standing grant, may run |
full | Anything may run, subject to ask |
ask — when a human is consulted.
off | Never |
on-miss | The first time each command is seen; the answer is remembered |
always | Every 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:
nouride grants # what has a standing yesnouride grants revoke <id>nouride grants revoke --allOr /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.
| Class | Examples |
|---|---|
destructive | recursive deletes, mkfs, overwriting a device |
system-control | reboot, shutdown, taking a service down, pct restart |
infrastructure | terraform apply, terraform destroy, kubectl delete, helm uninstall, ansible-playbook |
credentials | reading or writing credential paths |
runtime-self | this daemon’s own state |
Their read halves are not gated at all — terraform 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
nouride policy check rm -rf /tmp/buildAnswers “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.
nouride policy # what always asks, and what never doesThe 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, printenv | print 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 |
git | status 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_TOKENThe 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.
nouride audit # recent audited actionsnouride audit 100 # more of themThe 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”.