GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • ChatCLI home page
  • Cookbook
  • Releases
  • diillson/chatcli
  • Install
  • Quickstart
  • Docker & Kubernetes
  • Auto-Update
  • Config Migration
  • Plugin @coder
  • Atomic Tools
  • Coder Mode Security (Governance)
  • Permissions
  • Git Worktrees

This view

  • Original page
  • New address

GeezerVison is showing chatcli.edilsonfreitas.com — Coder Mode Security (Governance) - ChatCLI

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

Allow (Permitted)

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).

Deny (Blocked)

The action is silently blocked (or with an error for the AI). Ideal for protecting sensitive files or destructive commands.

Ask (Prompt)

ChatCLI pauses and displays an interactive menu for you to decide. This is the default for unconfigured actions.

​
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, @recall (reads back output this session compressed) and @memory recall/timeline/map/neighbors/list — all pass without a prompt while write/exec stay gated (@memory link/unlink change memory and stay gated like remember).
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.

​
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:
+========================================================+
|              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

SubcommandPrompt LabelDetails Displayed
execExecute shell command$ <command>, dir: <cwd>
testRun tests$ <command>, dir: <cwd>
writeWrite filefile: <path>
patchModify file (patch)file: <path>
readRead filefile: <path>
searchSearch codeterm: <pattern>, dir: <path>
treeList directory structuredir: <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:
+========================================================+
|              SECURITY CHECK                             |
+========================================================+
 Agent:  shell
 Task:   Run auth module tests
 --------------------------------------------------------
 Action: Execute shell command
         $ go test ./pkg/auth/...
 Rule:   no rule for '@coder exec'
 --------------------------------------------------------
This allows you to know exactly which agent is requesting the action and why, facilitating informed security decisions.

​
Options

1

y (Yes)

Executes only this time. Next time, it will ask again.
2

a (Always)

Creates a permanent ALLOW rule for this command (e.g., allows all writes with @coder write).
3

n (No)

Skips execution. The AI receives an error informing that the user denied.
4

d (Deny)

Creates a permanent DENY rule. The action will be automatically blocked in the future.
For exec commands, the “Always” and “Deny Forever” options are not available, as each execution is unique and requires individual approval.

​
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
A local coder_policy.json loads only from a trusted workspace, and it can only tighten the global policy — it can never widen it. This changed in the security release; the older merge/replace behaviour is gone.
  • A local rule may add an ask or deny on top of the global policy.
  • A local allow rule is ignored — a project file can never grant a permission your global policy does not already grant.
  • When you choose “Always allow” at a prompt while working in a project, that new allow rule is written to the global file (~/.chatcli/coder_policy.json), never to the project file.
  • In an untrusted folder the local file is not read at all (a warning is printed on stderr). Trust the folder first with chatcli trust [dir].
See the security hardening upgrade guide for the full workspace-trust model.
./coder_policy.json
{
  "rules": [
    { "pattern": "@coder write", "action": "ask" },
    { "pattern": "@coder exec --cmd 'rm -rf'", "action": "deny" }
  ]
}
The local file above only tightens: it forces @coder write back to ask and denies a destructive rm -rf even if the global policy was more permissive. Any allow it contained would be dropped.
For evals, the per-case policy no longer comes from a coder_policy.json placed in the sandbox. It comes from CHATCLI_CODER_POLICY_OVERLAY, a path read only from the process environment; that overlay is a full merge and is never persisted.

​
Policy Example (coder_policy.json)

{
  "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:
RuleCommandResult
@coder read = allow@coder read file.txtAllowed
@coder read = allow@coder readlink /tmpDoes not match (falls to Ask)
@coder read --file /etc = deny@coder read --file /etc/passwdDeny (path-prefix match)
This means @coder read will never allow @coder readlink or @coder readwrite accidentally.

​
Command Validation (50+ Patterns)

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

Data destruction

rm -rf, dd if=, mkfs, drop database

Remote execution

curl | bash, base64 | sh

Code injection

python -c, eval, $(curl ...)

Process substitution

<(cmd), >(cmd)

Kernel manipulation

insmod, modprobe, rmmod

Evasion

${IFS;cmd}, VAR=x; bash
You can add custom patterns via CHATCLI_AGENT_DENYLIST:
export CHATCLI_AGENT_DENYLIST="terraform destroy;kubectl delete namespace"
For the complete list of ChatCLI security protections, see the Security and Hardening documentation.

​
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:
ModeFile writesNetworkWhen to use
off (default)unrestrictedallowedTrust the validator + policy alone
workspaceconfined to the workspace and build cachesallowedContain accidental writes outside the project
strictconfined to the workspace and build cachescut (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:
PlatformBackendMechanism
macOSsandbox-exec (built-in)Generated SBPL profile: writes confined, network denied in strict
Linuxbubblewrap (bwrap)Read-only root, workspace + caches bound read-write, --unshare-net in strict
Any OS (Windows, *BSD, or as a fallback)Docker / PodmanWorkspace 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).
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.

​
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.
This mechanism is transparent — you don’t need to do anything. The security check appears exactly once per action.

​
Best Practices

1

Start with Caution

Keep write commands (write, patch, exec) as ask until you feel confident in the agent.
2

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).
3

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.
4

Safe Exec

@coder exec blocks dangerous patterns by default (50+ rules). A --allow-unsafe or --allow-sudo flag coming from the model is ignored — only the human operator can lift these guards, via CHATCLI_CODER_ALLOW_UNSAFE=true and CHATCLI_AGENT_ALLOW_SUDO=true in the environment. Set them deliberately and only when necessary.

​
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

RuleWorker Action
allowAction executed automatically by the worker
denyAction blocked; the worker receives [BLOCKED BY POLICY] and continues its flow
askThe worker pauses, the progress spinner is suspended, and the security prompt is displayed
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.

​
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.
When no human can answer, an ask now DENIES, fail-safe. This is the release default across the gateway, the server pipeline, ACP/MCP without a dialog, chatcli tool, and chatcli eval. To let an unattended surface auto-approve its ask verdicts, the operator opts in with CHATCLI_POLICY_AUTOMODE=true (process-wide) or /policy mode auto (per session). deny rules and the safety-immune list keep blocking regardless. See the security hardening upgrade guide.
SurfaceWhat 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 capabilityRaises 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 capabilityDenies unless automode is on (CHATCLI_POLICY_AUTOMODE=true or /policy mode auto); dangerous exec commands are still refused in-band
Gateway daemonDenies unless the operator enabled automode (CHATCLI_POLICY_AUTOMODE=true or /policy mode auto) — autonomy at the messaging edge is now an explicit opt-in, not the default
One-shot without a terminal (chatcli -p "/coder …" from a script, CI, a pipe or chatcli eval)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 automode (CHATCLI_POLICY_AUTOMODE=true / /policy mode auto), for what a headless run must do
exec commands never receive the always choices on any surface — the same restriction the interactive prompt applies, since any shell command could be destructive.
deny rules block on every surface, and the dangerous-command validation 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:
CommandEffect
/policy or /policy statusShow the current mode, rule count and policy file
/policy mode autoAutomode: every policy ask verdict auto-approves for this session
/policy mode interactiveBack to the default: ask prompts for approval
Automode is also what lets an unattended surface act on an ask instead of denying it fail-safe — set it process-wide with CHATCLI_POLICY_AUTOMODE=true, or per session with /policy mode auto. See Ask Rules on Unattended Surfaces. What automode does not change:
  • deny rules still block — they resolve before ask in the policy check.
  • The dangerous-command validation still applies.
  • Safety-immune operations (rm -rf, sudo, force push, …) still prompt — automode widens convenience, never the blast radius the safety-immune tier 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:
VariableValuesDescription
CHATCLI_CODER_UIfull (default), minimalInterface style
CHATCLI_CODER_BANNERtrue (default), falseShow/hide the cheat sheet
These settings appear in /status and /config.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.