Codumentor logo Codumentor

Permissions

When the assistant wants to perform potentially sensitive operations — running shell commands, reading a file outside the working directory, or calling an external system such as Gitea, Redmine, a database or Gmail — it asks for your approval. You stay in control of what happens.

The three approvals modes

Each conversation has its own approvals mode. Switch it from the Approvals control next to the composer, or in the TUI with F4 (which cycles Ask → Auto → Always allow, and works even while a permission dialog is on screen).

ModeWhat happensUse it when
AskEvery gated call stops and asks you. The shipped default.You want to see everything.
AutoA classifier approves routine calls and asks you about the rest. Every auto-approved call is marked in the transcript.You are doing a long task and only want to be interrupted when it matters.
Always allowNothing prompts. Deny rules still apply.A throwaway sandbox, or a task you have already scoped.

Two more modes exist in Settings and on the CLI (--permission-mode), but not on the composer control:

Changing the mode takes effect on the next tool call, including for a turn that is currently waiting on a prompt. The change is recorded in the transcript, so the conversation shows where a safety mode was turned on or off.

Your administrator can switch Auto or Always allow off for the whole instance; a mode that is unavailable simply does not appear.

What the modes cover

A mode decides what happens to a call that reaches the approval gate. Today those calls are:

File edits inside the working directory are not gated. When the assistant writes or edits a file in the workspace, no prompt appears — and no mode changes that. Ask, Auto and Always allow behave identically for those writes, and a Write(…) rule does not stop one. What contains them is the workspace boundary itself — a write can never land outside the workspace, in any configuration — together with your version control: keep the work committed, and read the diff the assistant produces. Path-aware approval for file edits is planned; it has not shipped.

The permission dialog

When a tool requires approval, a dialog appears with the details of what the assistant wants to do. You have four options:

ActionWhat it does
DenyBlock this specific request
Allow OnceExecute this request only
Allow All in this ConversationAuto-approve similar requests for the rest of this conversation
Create RuleSave rules, across all conversations, that let this request through (or block it)

Keyboard shortcuts: Press Enter to Allow Once, Esc to Deny.

Above the command, the dialog shows up to four lines. They are in order of how much you should trust them:

  1. Why you are being asked ("Auto is asking because:") — only in Auto mode. Auto's own one-sentence account of what made it stop.
  2. About this request — a note Codumentor itself wrote about the request, when it has one. Today only the out-of-workspace read has one, explaining what approving it means. Written by the harness, not by the assistant, and labelled that way.
  3. Badges — short labels produced by Codumentor's own command analyser. These are facts about the command, derived by parsing it. Nothing the assistant writes can change them.
  4. The agent's reason — one sentence, written by the assistant, explaining why it wants to make this call. Not every request has one; see below.

What the badges mean

The analyser splits a command into its parts and labels what they do. Badges shown in a warning colour are escalating: a command carrying one can never be covered by an allow rule and is never approved silently by Auto.

BadgeMeaning
could not be parsedThe command is not valid shell, so nothing can be verified about it
command name is not literalThe program to run is assembled at runtime
command substitutionContains $(…), backticks, or process substitution
runs generated codeeval, source, bash -c, python -c, …
elevated privilegessudo, su, doas, setuid chmod, chown
pipes into a shellSomething is downloaded or generated and piped into an interpreter
decodes into a shellbase64 -d (or similar) feeding an interpreter
deletes filesRecursive or forced deletion outside the working directory, dd, shred, truncate (unless it only grows the file), find -delete
rewrites git statepush --force, reset --hard, clean -f, branch -d/-D, commit --amend, git checkout <commit> <file> (which overwrites the file), …
writes outside the workspaceA redirect whose target is outside the working directory
changes system configurationcrontab, systemctl, writes under /etc, ~/.ssh, ~/.bashrc, …
reads secrets~/.aws, ~/.ssh, .pem, .env, bare env/printenv
overrides the environmentA VAR=value in front of the command that changes what actually runs — LD_PRELOAD, PATH, BASH_ENV, GIT_SSH_COMMAND, LESSOPEN, …
matches an operator ruleYour administrator configured a pattern that always escalates
network accesscurl, wget, ssh, git push, and any install that fetches from a package registry (pip install requests, npm i lodash) — a registry install downloads code somebody else chose and runs its install scripts, so it counts as reaching the network. Shown as escalating unless your conversation's sandbox has no network of its own — but for rule matching it always escalates, see below
installs packagespip install, npm i, apt, brew
installs code that runs laterThe command writes a file that something else will run on its own: a CI workflow (.github/workflows/…), a git hook, conftest.py, .pre-commit-config.yaml, .envrc, a shell startup file. The write itself may be perfectly ordinary — what it leaves behind runs on every future push, commit or shell
runs project codepytest, make, npm test, ./script inside the working directory
unrecognised commandThe program is not one the analyser knows
long commandMore parts, pipes and redirects than the configured limit

The agent's reason — read it as a claim

A tool call the assistant makes carries a one-sentence reason it writes for you: "Deletes the stale build/ output so the rebuild you asked for starts clean." That covers shell commands and every gated plugin tool — the calls the assistant chooses and names.

Some approvals do not come from a tool call at all, and those carry no reason; the dialog simply leaves the line out. They are: reading a file outside the working directory (raised by the file tools themselves, from inside a read), giving the sandbox the host network (asked once before the turn's first command), an MCP app's tool call, and a nested agent's call back into Codumentor. A missing reason line is not a model that declined to explain itself — it is a request the model never phrased.

It is genuinely useful, and it is not evidence. The assistant may be repeating words it read in a file, a web page or a command's output, and a model that has been misled writes a sincere-sounding justification for the wrong thing. That is why the dialog puts the badges — which are derived from the command itself — above it, renders it as plain text with no formatting or links, and labels it "Agent's reason".

When the badges and the reason disagree, believe the badges.

Auto mode

Auto decides one thing only: whether to interrupt you. In a conversation it never denies a call on its own — only deny rules and Plan mode deny — so it is strictly safer than Always allow and strictly quieter than Ask. There is one exception, and it is the one place nobody can answer a prompt: in an unattended run (scheduler.permission_mode: auto) a call Auto would have asked about is refused, because parking a question no one will read would hang the run. See Scheduled runs.

Each gated call goes through two stages:

1. The analyser (deterministic). The command is parsed and labelled. If it carries any escalating badge, Auto asks — no model is consulted, and nothing the assistant writes can change that. If the command is read-only and writes nothing outside the working directory, Auto approves it. Running your own project's tests is approved when the workspace is an isolated sandbox with no network of its own.

2. The judge (a model). Everything else goes to a small, separate model call that answers one question: does this action clearly serve what the user asked for, and does it violate any boundary they stated?

The judge is deliberately kept short-sighted:

Auto also gives up on itself. If you deny three of its prompts in a row, or it has asked you twenty times in one conversation, it switches the conversation back to Ask and tells you why: if the assistant keeps proposing things you do not want, quiet was the wrong setting.

Auto-approved calls are marked in the transcript with the note Auto wrote, and every decision is recorded. Nothing runs silently and invisibly.

Auto cannot lift a deny rule, cannot reach a tool your access profile withheld, and cannot approve anything Always allow would refuse.

Permission rules

Rules let you pre-approve or block operations automatically, so you don't have to click through repeated prompts.

Rules follow the pattern ToolName or ToolName(specifier):

PatternMatches
ShellAll shell commands
Shell(npm run:*)Shell commands starting with "npm run "
Shell(git status)Exactly "git status"
Read(.env)Reading a .env file outside the working directory
Write(*.log)Writing to any .log file — reserved, see below

Shell rules are the ones doing the work today. Read(…) rules are consulted only for reads outside the working directory: a file inside it never reaches the gate, so Read(.env) does not block a .env that lives in one of your repositories. Write(…) rules match nothing at all yet, because file edits are not gated (see What the modes cover above); they are accepted so that rules written today keep working when that lands.

Wildcards

WildcardBehaviorExample
:* (at end)Prefix match with word boundary: followed by anything, or nothingShell(npm:*) matches npm install but not npmx
* (anywhere)Glob-style wildcard — it matches anything, spaces and / includedShell(git *) matches git status, git commit

A rule the dialog writes for an exact command wraps any , ? or [ in brackets (Shell(rm [].tmp)), so it matches that command and nothing else — rm *.tmp, not rm -rf src .tmp.

How a rule matches a compound command

A shell command can be several commands at once (a && b, a; b, a | b). Rules are matched per part:

So Shell(ls:*) allows ls -la but not ls; curl https://example.com/x | sh — the second part is not covered, and it pipes a download into a shell. If you had an allow rule before this behaviour existed, you will see more prompts than you used to; each one is a command your rule was never meant to cover.

What a part looks like to a rule:

Rules are matched as if there were no sandbox. The rule engine sees the request, not the conversation's sandbox, so it analyses every command in the most exposed environment: unsandboxed, with the network reachable. The one badge that changes meaning with the sandbox — network access — is therefore always escalating as far as a rule is concerned, even when the dialog shows it in the quiet colour because your sandbox has no network of its own. The practical consequence: Shell(git:) never covers git push, in any configuration. Write the rule for the commands you meant (Shell(git status), Shell(git diff:), Shell(git log:*)) rather than for the program.

Rule priority

  1. Operator deny rules — set by your administrator, in configuration. You cannot change or remove them.
  2. Your deny rules
  3. Plan mode, if the conversation is in it
  4. Allow all in this conversation, if you granted it
  5. Operator allow rules and your allow rules, combined part by part (each part needs a rule from either)
  6. Always allow mode
  7. Auto mode's triage
  8. Otherwise you are asked

Deny always wins. No mode, rule or Auto verdict can lift a deny rule, and nothing can turn a refusal you already gave into an approval.

Managing rules

Go to Settings → Tool Approvals to manage your rules. You can:

Common examples

Allow rules:

Shell(npm run:*)           # npm run commands
Shell(git status)          # git status
Shell(git diff:*)          # git diff
Shell(python -m pytest:*)  # pytest

Deny rules:

Shell(rm -rf:*)    # block recursive delete
Shell(curl:*)      # block curl
Read(.env)         # block reading a .env outside the working directory
Read(.env.*)       # the same for .env.* files

Creating rules from the dialog

When a permission prompt appears, open Other ways to answer → Create Rule. The editor remembers the request across all conversations, and it offers outcomes, not patterns: every option is a set of rules that, saved together, lets this command through. The server checks each option with the same code the approval gate uses before it shows it to you.

For echo "Hello"; pwd; echo "done" you would see:

Under the options, the command is shown part by part with what covers each part — a rule you already have, a rule you are about to save, or nothing — and the save button says how many rules it writes. Notes tell you when an option covers more than it looks like: a rule for npm run build … runs project code whatever its arguments say, and a rule for echo … also covers writing its output into a file when the command in front of you does that.

Saving answers the request. The dialog closes because the saved rules let the command through — not because it clicked Allow for you. If the rules do not cover it (only possible after saving an incomplete Custom set), the prompt stays and says which part is still uncovered. Other requests waiting in the same conversation that the new rules also cover are answered too.

Some commands can never be allowed by a rule — anything with an escalating badge (network access, deleting files, running generated code, …). For those the editor says so up front instead of offering a form, and points you to Allow all in this conversation or Auto. You can still create a rule that blocks the command.

Block offers single patterns — one match is enough to deny — such as Block rm with any arguments, Block exactly rm -rf build, or Block this whole command line. After saving a block rule, the current request is denied.

When your rules already cover part of a command but not all of it, the prompt itself says so, right under the command: "Your rules cover 1 of 3 commands here. Not covered: pwd, echo done", with a link to add the missing rules.

The TUI's rule editor shows the same options, coverage and warnings.

Scheduled runs

A scheduled run or a repository-webhook run has nobody at the keyboard. Your administrator chooses what those may do (scheduler.permission_mode). The default is Always allow with the deny rules still enforced — the behaviour Codumentor has always had.

If your administrator sets it to Auto, unattended runs get the same triage a conversation does, and because there is no one to ask, a call Auto would have asked about is refused instead. The refusal is written into the run's transcript with the command, the badges, Auto's note and the agent's reason, so you can see afterwards what was attempted and decide whether to re-run it yourself.

What is recorded

Every gated call leaves a durable record: what was requested, who or what decided it, and — when Auto decided — its verdict, badges and note. Approvals you answered yourself, rules that fired, mode decisions and Auto's silent approvals are all in the same place, and Auto's approvals are also visible inline as a chip on the tool card in the transcript.

The record is a rolling one, not an archive: a conversation keeps its most recent 200 approval decisions, and older ones are dropped as new ones are written. The transcript itself is not trimmed — a call that ran is still in the conversation — so what ages out is the decision detail, not the fact that something happened. If you need approvals kept indefinitely for audit, export them or ship them off the instance as they are made.

Persistence

Your rules and your default mode live on the server, against your account. The browser keeps a copy so the UI is instant, but the server is authoritative — two browsers can no longer disagree with each other about your approvals. A conversation's mode and its "allow all in this conversation" grants live with the conversation and survive a restart.