Permission modes & rules
Nexrall Code decides what runs automatically and what needs your approval through a combination of modes (the session's overall posture) and rules (allow / ask / deny patterns that match specific tools and arguments).
Modes
The mode sets how eager the agent is to act without asking:
| Mode | Behavior |
|---|---|
| Auto | The agent edits files and runs commands directly, prompting only for risky or unapproved actions. |
| Ask | Asks for approval before every edit and every shell command. Reads are free. |
| Edit | Edits are permitted; still asks before running commands. |
| Plan | The agent researches and proposes an approach without writing any files — every write-capable tool is hard-blocked, not just discouraged by instruction. |
Plan mode: questions and approval
In Plan mode the agent can still ask you (ask_user_question, a short multiple-choice card) when a request is ambiguous or there is a real fork between approaches — it is told to ask rather than guess. When the plan is ready it calls exit_plan_mode, which ends the turn and shows an approval prompt:
- Yes, auto-accept edits — leaves Plan for Edit mode.
- Yes, ask before each edit — leaves Plan for Ask mode.
- No, keep planning — stays read-only so you can say what to change.
The approved plan itself is sent as the next prompt. Approval never jumps to Auto/bypass. A Plan-mode reply that only answers a question shows no approval prompt.
Switch modes with /mode in a session, or in VS Code from the chat panel.
Rules
Rules are the fine-grained control. Each rule matches a tool and, optionally, a specific invocation, and says what to do:
allow— run without prompting.ask— ask for confirmation first.deny— refuse outright.
Rules live in settings.json under permissions, and merge across tiers (managed → user → project) with one hard rule: deny always wins, then allow, then ask.
{
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test *)"
],
"ask": [
"Bash(curl *)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(secrets/**)"
]
}
}Rule syntax
- Bare tool name — matches every invocation:
"Read"matches all reads. - Scoped with
(pattern)— matches only invocations whose argument matches the pattern:Bash(npm run test *)allows test runs but not othernpm runcommands. *wildcard — matches anything within the scoped pattern. In a command pattern (Bash(…)) it matches any characters,/included:Bash(rm *)coversrm -rf dir/sub, andBash(curl *)covers a URL. In a path pattern (Read(…),Edit(…)) it stays within one path segment; use**to cross directories.- Parameter match —
denyandaskrules can also match a raw input parameter withTool(param:value), value wildcards included:task(subagent_type:explorer)refuses that sub-agent type andtask(run_in_background:true)refuses background dispatches. The name must be an actual parameter of the call — a rule naming one this tool does not have simply does not match (it never degrades to "any input").allowrules do not take this form; they stay exactly as written.
How patterns match a command — a deny or ask rule also matches the command when it is dressed up or chained: Bash(rm:*) refuses FOO=bar rm -rf build, sudo rm -rf build, cd /tmp && rm -rf build and echo $(rm -rf build) — the same spellings the destructive guard already reads. allow rules are deliberately exact: they match what was written (Bash(sudo npm run test:*) matches sudo npm run test), so an environment prefix — which can run code of its own, e.g. NODE_OPTIONS — never rides an allow rule without asking.
Paths behind symlinks get the same treatment: deny/ask rules aimed at the real target (Read(secrets/**)) also match when the file is reached through a linked directory (and a file being created through one resolves via its parent). allow again stays literal — an exception is only as wide as it was written.
Built-in safety guardrails
Independent of your rules, Nexrall Code hard-blocks or double-checks a few classes of action:
- Destructive commands — irreversible operations (force-push, drop table,
terraform destroy) require explicit confirmation even if they'd otherwise match anallowrule. - Protected paths — writes to configuration files that can run code (shell startup files, git config, package-manager/build-tool configs,
.nexrall/and friends) are never auto-approved — see below. - Silent code loss — an edit that drops existing code behind a placeholder like
// rest unchangedis refused. - Test-integrity — weakening a test to make it pass (deleting an assertion, adding
.skip) is flagged, not silently accepted.
Protected paths
A small, fixed set of paths is never auto-approved — not by a mode blanket, not by an allow rule, not by an "always" answer:
- Shell startup files —
.zshenv,.zshrc,.bashrc,.profile,.envrc, … (matched by file name at any depth). - Git config & internals —
.gitconfig,.gitmodules, anything under.config/git/, and.git/itself. - Package-manager / build-tool / hook configs —
.npmrc,.yarnrc*,.pnpmfile.cjs,bunfig.toml,.bazelrc,gradle-wrapper.properties,.pre-commit-config.yaml,lefthook.yml, … - Agent, editor & tooling config dirs —
.nexrall/,.claude/,.vscode/,.idea/,.husky/,.cargo/,.devcontainer/,.yarn/,.mvn/, plus.mcp.jsonand.claude.json.
These are the files a plausible-looking write ("update .npmrc") can use to repoint a package registry, plant code that runs on the next shell start or the next commit, or hand the agent new permissions — exactly the shape a prompt injection needs.
What each context does with one:
| Context | Protected-path write |
|---|---|
| Ask / Edit | Always prompts |
| Auto (classifier on) | The classifier reviews it first — its hard limits can refuse it outright — and the write still prompts (stricter than Claude Code, which lets its auto classifier stand in) |
| Auto (classifier off) | Always prompts |
| Plan | Refused along with every other mutation |
| Headless / CI | Denied; a PermissionRequest hook or --permission-prompt-tool may still answer explicitly, and --yolo is the blanket opt-out |
--yolo / bypassPermissions | Runs without asking |
Symlinks resolve before the check: a write through a link whose real target is protected prompts the same way — a guard like this must see the file it would actually write. A session that runs inside a worktree (.nexrall/worktrees/<name>/) edits its checkout normally; the worktree's own .nexrall/ config is still protected.
In headless mode (scripts, CI — where there's no one to confirm), destructive operations are blocked by default; set NEXRALL_ALLOW_DESTRUCTIVE=1 only in a trusted context. See Environment variables.
Auto-mode classifier
Auto mode is the default, and it is the mode where a wrong action hurts the most: the agent runs routine things without asking. On top of your rules and the guardrails above, auto mode routes each remaining mutating action through a small classifier — one short model call that reviews the action against:
- Hard limits — blocked unconditionally (data exfiltration, touching credential/system files outside the project, tampering with the safety system). Explicit user intent does not clear these.
- Soft limits — blocked unless your latest message directly and specifically asks for that exact action (force-push, database/infra destruction, mass deletion outside the workspace), or an allow exception matches.
- Allow exceptions and trusted environment — prose entries that tell the classifier what is routine for you (your repos, buckets, deploy targets).
Ordering, matching Claude Code's auto mode: deny rules → the destructive-guard floor (still a typed "yes") → narrow allow rules → the classifier → the permission prompt. Broad allow rules (Bash(*), bare Bash, wildcarded interpreters like Bash(python:*)) are suspended while the classifier is on — they are exactly what a prompt injection would want; narrow rules keep working. --yolo skips the classifier (that is what the flag means); the destructive guard still applies.
Read-only calls and edits inside the workspace are never reviewed — that is the "file edits proceed" promise, kept for free. Repeat commands are cached, and each session is budgeted (defaults: 400 reviews / $0.25). When the review cannot happen — model unreachable, budget spent — the session asks instead (headless: denies). It never silently allows.
Two things worth knowing:
autoModesettings are read from your user settings, managed settings and--settingsonly. A project's.nexrall/settings.jsonthat setsautoModeis ignored (andnex doctorsays so) — a cloned repo must never be able to loosen this.- The kill switch is
NEXRALL_AUTO_CLASSIFIER=0, or"classifier": falseunderautoMode.
{
"autoMode": {
"classifier": true,
"classifierModel": "your-cheap-model-id",
"environment": ["We deploy from GitHub Actions to Cloudflare; buckets live in acme-assets"],
"allow": ["terraform plan"],
"soft_deny": ["$defaults", "touching the payments repo"],
"hard_deny": ["$defaults"],
"classifyAllShell": false
}
}Manage it from the shell: nex auto-mode defaults (the built-in limits), nex auto-mode config (what is in effect, from which file), nex auto-mode critique (a model review of your config), nex auto-mode reset (drop the user block). "$defaults" keeps the built-ins alongside your entries; leaving it out replaces them. Design notes: AUTO_MODE_CLASSIFIER.md.
Headless / CI
nex --output-format json "…" implies auto-approve (there's no one to prompt), but the destructive-command guard still holds unless explicitly relaxed, and writes to protected paths are denied — nobody is there to confirm them. See Nexrall CLI → Safety in headless mode.
Next steps
- Configuration — where rules live and how tiers merge.
- Environment variables — the safety toggles (
NEXRALL_ALLOW_DESTRUCTIVE,NEXRALL_SECURITY_LINT, …).