Skip to content

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:

ModeBehavior
AutoThe agent edits files and runs commands directly, prompting only for risky or unapproved actions.
AskAsks for approval before every edit and every shell command. Reads are free.
EditEdits are permitted; still asks before running commands.
PlanThe 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.

json
{
  "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 other npm run commands.
  • * wildcard — matches anything within the scoped pattern. In a command pattern (Bash(…)) it matches any characters, / included: Bash(rm *) covers rm -rf dir/sub, and Bash(curl *) covers a URL. In a path pattern (Read(…), Edit(…)) it stays within one path segment; use ** to cross directories.
  • Parameter match — deny and ask rules can also match a raw input parameter with Tool(param:value), value wildcards included: task(subagent_type:explorer) refuses that sub-agent type and task(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"). allow rules 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 an allow rule.
  • 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 unchanged is 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.json and .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:

ContextProtected-path write
Ask / EditAlways 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
PlanRefused along with every other mutation
Headless / CIDenied; a PermissionRequest hook or --permission-prompt-tool may still answer explicitly, and --yolo is the blanket opt-out
--yolo / bypassPermissionsRuns 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:

  • autoMode settings are read from your user settings, managed settings and --settings only. A project's .nexrall/settings.json that sets autoMode is ignored (and nex doctor says so) — a cloned repo must never be able to loosen this.
  • The kill switch is NEXRALL_AUTO_CLASSIFIER=0, or "classifier": false under autoMode.
json
{
  "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 ​

Built by Maxrall, Inc.