> ## Documentation Index
> Fetch the complete documentation index at: https://chatcli.edilsonfreitas.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Coder Mode Security (Governance)

> Understand how the governance and permissions system of the Coder Mode works.

ChatCLI was designed to be a powerful tool, but power requires control. In **Coder Mode** (`/coder`), the AI has the ability to read, write, create, and execute commands on your system. To ensure you are always in control, we implemented a governance system inspired by ClaudeCode.

***

## How Does It Work?

Every time the AI suggests an action (such as creating a file or running a script), ChatCLI checks your local security rules before executing.

### The 3 Permission States

<CardGroup cols={3}>
  <Card title="Allow (Permitted)" icon="circle-check">
    The action is executed automatically, without interruption. Ideal for read commands (`read`, `tree`, `search`) and read-only Git operations (`git-status`, `git-diff`, `git-log`, `git-changed`, `git-branch`).
  </Card>

  <Card title="Deny (Blocked)" icon="circle-xmark">
    The action is silently blocked (or with an error for the AI). Ideal for protecting sensitive files or destructive commands.
  </Card>

  <Card title="Ask (Prompt)" icon="circle-question">
    ChatCLI pauses and displays an interactive menu for you to decide. This is the **default** for unconfigured actions.
  </Card>
</CardGroup>

### policy\_manager priority order

When the agent requests an action, `policy_manager` evaluates rules in the order below (first match wins):

1. **Deny rules** — explicit user rules always win
2. **Safety-immune** — operations that ALWAYS prompt (`@coder exec`)
3. **Explicit allow vs ask** — longest pattern wins
4. **Read-only exec heuristic** — `@coder exec` with a provably read-only command auto-allows
5. **Capability gate** (new) — plugins that declare `IsReadOnly=true` via the capability interface auto-allow
6. **Default** — Ask

Layer 5 covers `@read`, `@search`, `@tree`, `@websearch`, `@webfetch` (GET), `@scheduler query/list`, `@coder read/search/tree` — all pass **without a prompt** while write/exec stay gated.

<Tip>
  Deny rules always beat the capability gate. You can block `@websearch` in a corporate environment via a `@websearch → Deny` rule even if the plugin advertises read-only.
</Tip>

### Typeahead protection (input guard)

When the LLM is streaming and the security box appears, any character you typed **before** the prompt rendered is discarded automatically. Three layers:

* **Kernel TTY flush** — `TCIFLUSH` (Linux), `TIOCFLUSH` (BSD/Darwin), `FlushConsoleInputBuffer` (Windows) clears the kernel's input queue.
* **Channel drain** — non-blocking drain of the centralized stdin reader channel (10-line buffer).
* **Intent debounce** — discards any input arriving in the first 250ms after the prompt mounts (humans don't react that fast to a new UI).

Without this, accidentally typing during the LLM stream would let the security box consume the buffered bytes as the y/n answer.

**Instructions are kept, answers are not.** A complete line you submitted for the agent (for example `also update the changelog`) never answers the prompt, but it is no longer thrown away: the drain re-queues it and it reaches the model at the next turn boundary. From the drain, only lines shaped like a prompt answer (`y`, `n`, `yes`, `no`, `sim`, `a`, `always`, `d`, `deny`, or a bare Enter) stay discarded.

***

## Interactive Approval Menu

When an action falls into the "Ask" state, you will see a security box with contextual information about the action:

```text theme={"system"}
+========================================================+
|              SECURITY CHECK                             |
+========================================================+
 Action: Write file
         file: main.go
 Rule:   no rule for '@coder write'
 --------------------------------------------------------
 Choice:
   [y] Yes, execute (once)
   [a] Always allow (@coder write)
   [n] No, skip
   [d] Always block (@coder write)

 > _
```

The prompt displays the **action in human language** (e.g., "Write file", "Execute shell command", "Modify file (patch)") and the **parsed details** from the JSON arguments, instead of showing raw JSON.

#### Recognized Action Types

| Subcommand | Prompt Label | Details Displayed |
| - | - | - |
| `exec` | Execute shell command | `$ <command>`, `dir: <cwd>` |
| `test` | Run tests | `$ <command>`, `dir: <cwd>` |
| `write` | Write file | `file: <path>` |
| `patch` | Modify file (patch) | `file: <path>` |
| `read` | Read file | `file: <path>` |
| `search` | Search code | `term: <pattern>`, `dir: <path>` |
| `tree` | List directory structure | `dir: <path>` |

### Prompt with Context in Parallel Mode

When the action is requested by a **multi-agent mode worker**, the prompt includes additional information about which agent is making the request:

```text theme={"system"}
+========================================================+
|              SECURITY CHECK                             |
+========================================================+
 Agent:  shell
 Task:   Run auth module tests
 --------------------------------------------------------
 Action: Execute shell command
         $ go test ./pkg/auth/...
 Rule:   no rule for '@coder exec'
 --------------------------------------------------------
```

<Info>This allows you to know **exactly** which agent is requesting the action and why, facilitating informed security decisions.</Info>

### Options

<Steps>
  <Step title="y (Yes)">
    Executes only this time. Next time, it will ask again.
  </Step>

  <Step title="a (Always)">
    Creates a permanent **ALLOW** rule for this command (e.g., allows all writes with `@coder write`).
  </Step>

  <Step title="n (No)">
    Skips execution. The AI receives an error informing that the user denied.
  </Step>

  <Step title="d (Deny)">
    Creates a permanent **DENY** rule. The action will be automatically blocked in the future.
  </Step>
</Steps>

<Note>For `exec` commands, the "Always" and "Deny Forever" options are not available, as each execution is unique and requires individual approval.</Note>

***

## Rule Management

Rules are saved locally in `~/.chatcli/coder_policy.json`. You can edit this file manually if desired, but the interactive menu is the easiest way to configure.
The matching uses the effective @coder subcommand even when `args` is JSON (e.g., `{"cmd":"read"}` becomes `@coder read`).

### Local Policy (Per Project)

You can add a local policy in the project directory:

* Local: `./coder_policy.json`
* Global: `~/.chatcli/coder_policy.json`

<Tabs>
  <Tab title="With merge (local + global)">
    If `merge: true`, local rules **merge** with global ones (local overrides matching patterns).

    ```json theme={"system"}
    {
      "merge": true,
      "rules": [
        { "pattern": "@coder write", "action": "ask" },
        { "pattern": "@coder exec --cmd 'rm -rf'", "action": "deny" }
      ]
    }
    ```
  </Tab>

  <Tab title="Without merge (local only)">
    If `merge: false` or absent, **only** the local policy is used.

    ```json theme={"system"}
    {
      "rules": [
        { "pattern": "@coder read", "action": "allow" },
        { "pattern": "@coder write", "action": "ask" }
      ]
    }
    ```
  </Tab>
</Tabs>

### Policy Example (coder\_policy.json)

```json theme={"system"}
{
  "rules": [
    {
      "pattern": "@coder read",
      "action": "allow"
    },
    {
      "pattern": "@coder git-status",
      "action": "allow"
    },
    {
      "pattern": "@coder write",
      "action": "ask"
    },
    {
      "action": "deny",
      "pattern": "@coder exec --cmd 'rm -rf'"
    }
  ]
}
```

***

## Word Boundary Matching

The policy system uses **word boundary matching**, ensuring that rules do not partially match different subcommands:

| Rule | Command | Result |
| - | - | - |
| `@coder read` = allow | `@coder read file.txt` | Allowed |
| `@coder read` = allow | `@coder readlink /tmp` | **Does not match** (falls to Ask) |
| `@coder read --file /etc` = deny | `@coder read --file /etc/passwd` | Deny (path-prefix match) |

<Tip>This means `@coder read` will **never** allow `@coder readlink` or `@coder readwrite` accidentally.</Tip>

***

## Command Validation (50+ Patterns)

Beyond policy governance, `@coder exec` validates each command against **50+ regex patterns** that detect:

<CardGroup cols={2}>
  <Card title="Data destruction" icon="trash">
    `rm -rf`, `dd if=`, `mkfs`, `drop database`
  </Card>

  <Card title="Remote execution" icon="globe">
    `curl | bash`, `base64 | sh`
  </Card>

  <Card title="Code injection" icon="syringe">
    `python -c`, `eval`, `$(curl ...)`
  </Card>

  <Card title="Process substitution" icon="arrows-left-right">
    `<(cmd)`, `>(cmd)`
  </Card>

  <Card title="Kernel manipulation" icon="microchip">
    `insmod`, `modprobe`, `rmmod`
  </Card>

  <Card title="Evasion" icon="mask">
    `${IFS;cmd}`, `VAR=x; bash`
  </Card>
</CardGroup>

You can add custom patterns via `CHATCLI_AGENT_DENYLIST`:

```bash theme={"system"}
export CHATCLI_AGENT_DENYLIST="terraform destroy;kubectl delete namespace"
```

<Info>For the complete list of ChatCLI security protections, see the [Security and Hardening documentation](/security/overview).</Info>

***

## Exec Sandbox (OS-level confinement)

The command validator and the permission policy gate **what** a command is allowed to run. The exec sandbox is a second, complementary layer that confines **what a permitted command can touch** — file writes and network — using the operating system's own sandboxing.

It is **opt-in and default OFF**, selected with `CHATCLI_CODER_SANDBOX`:

| Mode | File writes | Network | When to use |
| - | - | - | - |
| `off` (default) | unrestricted | allowed | Trust the validator + policy alone |
| `workspace` | confined to the workspace **and build caches** | allowed | Contain accidental writes outside the project |
| `strict` | confined to the workspace and build caches | **cut** (no network) | Fully offline, contained builds and tests |

In `workspace` and `strict`, writes are allowed to the workspace plus a generous allowlist of toolchain caches — Go module cache / `GOCACHE`, `.npm`, `.cargo`, `.rustup`, `.gradle`, `.m2`, `~/.chatcli`, and the temp dir — so ordinary builds keep working while stray writes elsewhere are blocked.

### Backends — confinement on any OS

ChatCLI picks the strongest available backend for the host, and the container backend makes confinement work **on any operating system, Windows included**:

| Platform | Backend | Mechanism |
| - | - | - |
| **macOS** | `sandbox-exec` (built-in) | Generated SBPL profile: writes confined, network denied in `strict` |
| **Linux** | `bubblewrap` (`bwrap`) | Read-only root, workspace + caches bound read-write, `--unshare-net` in `strict` |
| **Any OS** (Windows, \*BSD, or as a fallback) | **Docker / Podman** | Workspace mounted read-write in a container, `--network none` in `strict` |

**Selection order:** an explicit container request (`CHATCLI_CODER_SANDBOX=docker`, `podman`, or `container`) wins; otherwise the native backend is used; if none is present, the portable container backend takes over; only when **no backend at all** is installed does it degrade to unconfined — printing a one-line note, never failing the command.

The container image is configurable with `CHATCLI_CODER_SANDBOX_IMAGE` (default `alpine:3`).

<Warning>
  The sandbox is **opt-in** on purpose: a silently confined build that can't reach its module cache is worse than no sandbox at all. Turn it on deliberately.

  With the **container backend**, the command runs inside the image — so host toolchains (`go`, `node`, …) are only available if the image ships them. Point `CHATCLI_CODER_SANDBOX_IMAGE` at an image that carries your toolchain, or use the native macOS/Linux backends when you need the host tools.
</Warning>

***

## Tool Call Deduplication

ChatCLI includes a deduplication mechanism that ensures each tool call is verified and executed **exactly once**.

### The Problem

When the AI responds with XML-format tool calls (e.g., `<tool_call name="@coder" args='{"cmd":"read",...}' />`), ChatCLI's parser uses two extraction methods:

1. **XML Parser** — extracts complete `<tool_call>` tags
2. **JSON Parser** — looks for JSON objects that represent tool calls (for models that respond in pure JSON)

The JSON embedded inside the XML `args` attribute (e.g., `{"cmd":"read",...}`) could be incorrectly recognized by the JSON parser as a second tool call. Without deduplication, this would cause:

* The security check displayed **twice** for the same action
* The same action executed **twice**

### The Solution

`ParseToolCalls` applies automatic deduplication: tool calls extracted by the JSON parser that already exist as part of an XML tool call are discarded. The comparison uses the tool name, arguments, and raw text to detect duplicates.

<Info>This mechanism is transparent — you don't need to do anything. The security check appears exactly once per action.</Info>

***

## Best Practices

<Steps>
  <Step title="Start with Caution">
    Keep write commands (`write`, `patch`, `exec`) as `ask` until you feel confident in the agent.
  </Step>

  <Step title="Allow Reads">
    Generally, it is safe to give "Always" for `coder read`, `coder tree`, `coder search`, and read-only Git (`git-status`, `git-diff`, `git-log`).
  </Step>

  <Step title="Be Specific">
    Matching uses word boundary for subcommand prefixes and path-prefix for arguments. You can allow `coder exec --cmd 'ls` but block `coder exec --cmd 'rm`.
  </Step>

  <Step title="Safe Exec">
    `@coder exec` blocks dangerous patterns by default (50+ rules). Use `--allow-unsafe` only when necessary.
  </Step>
</Steps>

***

## Governance in Multi-Agent Mode (Parallel)

Security policies are **fully respected** by multi-agent mode workers. When `/coder` or `/agent` operates in parallel mode, each worker checks the `coder_policy.json` rules before executing any action.

### Behavior

| Rule | Worker Action |
| - | - |
| **allow** | Action executed automatically by the worker |
| **deny** | Action blocked; the worker receives `[BLOCKED BY POLICY]` and continues its flow |
| **ask** | The worker **pauses**, the progress spinner is suspended, and the security prompt is displayed |

<Note>Security prompts from multiple workers are **serialized** -- only one prompt at a time is displayed, avoiding visual overlap. Rules created during the session (via "Always" or "Deny") are immediately visible to all subsequent workers.</Note>

***

## Ask Rules on Unattended Surfaces (ACP, MCP, Gateway, headless one-shot)

In the interactive terminal an `ask` rule shows the security prompt. On unattended surfaces — where stdin carries a protocol or nobody is watching — the rule is resolved per surface:

| Surface | What an `ask` rule does |
| - | - |
| **ACP** (IDE) | Raises the IDE's native dialog via `session/request_permission` with the full terminal vocabulary — *Allow* / *Always allow* / *Reject* / *Always reject*; the *always* choices persist a rule exactly like `a`/`d` in the terminal prompt. A denial blocks and the model replans. The wait is bounded by the same `CHATCLI_MCP_PERMISSION_TIMEOUT` (default `600s`); an unanswered dialog denies fail-safe with unanswered-request feedback |
| **MCP** with `elicitation` capability | Raises an `elicitation/create` form in the client with the same four-way decision; *always* choices persist rules. The wait is bounded by `CHATCLI_MCP_PERMISSION_TIMEOUT` (default `600s`): an unanswered form denies fail-safe and the model is told the request went unanswered. `CHATCLI_MCP_ELICITATION=off` disables the forms entirely |
| **MCP** without the capability | Auto-approves (historical contract); dangerous exec commands are still refused in-band |
| **Gateway daemon** | Auto-approves — the operator opted into full autonomy at the messaging edge |
| **One-shot without a terminal** (`chatcli -p "/coder …"` from a script, CI, a pipe or [`chatcli eval`](/agents/harness/evals)) | Denies at once, fail-safe: nobody can answer the prompt, which would otherwise wait until the run's timeout. Parallel workers get the same denial. The model is told the action needs approval and was not executed (not that you refused it), and continues without it. Use an explicit `allow` rule, or session automode, for what a headless run must do |

<Note>`exec` commands never receive the *always* choices on any surface — the same restriction the interactive prompt applies, since any shell command could be destructive.</Note>

`deny` rules block on **every** surface, and the [dangerous-command validation](#command-validation-50-patterns) applies everywhere regardless of policy. A client that doesn't implement the permission round-trip (answers *method not found*) falls back to the auto-approve contract instead of having every gated action denied.

***

## Session Policy Mode (`/policy`)

Sometimes you want a session to just *flow* — a long refactor you already trust, a scripted run — without approving every `ask` one by one. `/policy` switches the **session-scoped** policy mode:

| Command | Effect |
| - | - |
| `/policy` or `/policy status` | Show the current mode, rule count and policy file |
| `/policy mode auto` | **Automode**: every policy `ask` verdict auto-approves for this session |
| `/policy mode interactive` | Back to the default: `ask` prompts for approval |

What automode does **not** change:

* **`deny` rules** still block — they resolve before `ask` in the policy check.
* The **[dangerous-command validation](#command-validation-50-patterns)** still applies.
* **Safety-immune operations** (`rm -rf`, `sudo`, force push, …) still prompt — automode widens convenience, never the blast radius the [safety-immune tier](#policy_manager-priority-order) protects against.

The mode is **never persisted**: every new session starts interactive. Every auto-approval is logged for auditability, and the current mode is visible in `/policy status` and `/config security`.

Available on all three surfaces: the terminal REPL, the ACP slash-command menu in the IDE, and MCP via the `manage_session` tool's `policy_mode` action (`name: auto | interactive | status`).

***

## Coder Mode UI

You can control the style and banner of `/coder` via environment variables:

| Variable | Values | Description |
| - | - | - |
| `CHATCLI_CODER_UI` | `full` (default), `minimal` | Interface style |
| `CHATCLI_CODER_BANNER` | `true` (default), `false` | Show/hide the cheat sheet |

<Tip>These settings appear in `/status` and `/config`.</Tip>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.