Skip to main content
ChatCLI is built with a defense-in-depth security architecture. This page documents every protection layer, how to configure them, and best practices for production environments.
Status means what it says. Active is on with no configuration. Opt-in exists and does nothing until you turn it on — most of the strongest controls here are opt-in on purpose, because the alternative is a default that surprises someone in production. Where a control is opt-in, the row says so and the section says how to enable it.

Security Overview

The table below summarizes all active protections across every layer of the stack.

Authentication and Authorization

The gRPC server verifies JWTs with configurable issuer, audience, and either a shared secret (HS256) or an RSA public key (RS256). JWTs carry a role claim that maps to an RBAC level.
The algorithm comes from configuration, never from the token. A verifier that reads alg to decide how to check a signature is one an attacker chooses for: the RSA public key is public, so an HS256 token signed with that key as the HMAC secret would verify. ChatCLI configures exactly one algorithm and refuses any token declaring another — including none.Setting CHATCLI_JWT_PUBLIC_KEY selects RS256. Setting only CHATCLI_JWT_SECRET selects HS256, unless its value resolves to PEM key material, in which case it selects RS256 too. /config server prints which algorithm is in effect.
Issuer and audience are only checked when configured. Leaving them empty accepts any iss and aud, which means a token minted for a different service by the same issuer is accepted — set both wherever a signing key is shared.

RBAC Roles

Three built-in roles control access to API endpoints and operations: Each level has two accepted spellings, and they are exact aliases — viewer and readonly grant the same thing.
An unrecognised role resolves to read-only. A token whose role claim is a value this server does not know — a typo in an issuer’s configuration, or a role from another system — is granted the lowest level, and the server logs the claim it did not recognise. That is the direction such a mistake has to fail; the alternative is that misspelling viewer grants write access.A token carrying no role claim at all keeps the historical operational level, so tokens minted before roles existed are not locked out on upgrade. Issue tokens with an explicit role.
The /Health endpoint is always accessible without authentication to support load balancer and orchestrator health checks.

Legacy Bearer Token

For simpler deployments, the server supports static bearer token authentication with constant-time comparison (crypto/subtle.ConstantTimeCompare), preventing timing attacks.

OAuth 2.0 + PKCE

ChatCLI supports OAuth 2.0 with PKCE for the following providers:
OAuth tokens are automatically refreshed before expiration. The refresh flow uses a plain HTTP client (no logging transport) with the appropriate User-Agent header to avoid Cloudflare issues.

Encryption and Data Protection

AES-256-GCM Credential Encryption

All OAuth credentials are encrypted at rest using AES-256-GCM in ~/.chatcli/auth-profiles.json. The encryption key is automatically generated and stored with strict permissions.

Encryption at Rest (sessions, memory, contexts, archives, costs)

One key covers every store, derived per store class with HKDF — there is no separate key per profile. It is opt-in and off until CHATCLI_ENCRYPTION_KEY is set.
Version 2 payloads are bound to their store. A sealed file carries, as authenticated data, its relative store path and tenant slug: a session, memory or park file copied from one tenant’s directory into another’s (or renamed) no longer opens as if it belonged there. Secrets shorter than 32 bytes are treated as passphrases and stretched with Argon2id before key derivation; a random key of at least 32 bytes keeps the direct derivation, so the documented key stays best practice. Version-1 payloads keep loading and are rewritten as bound v2 by their next save or by /config security reseal, which now fsyncs. Tenant roots carry a 16-byte digest (roots created with the older short digest keep being used). Encryption at rest is an explicit opt-in: it is active while CHATCLI_ENCRYPTION_KEY is set in the process environment. When it is, every store that embeds conversation content is sealed before it touches disk and opened transparently on read:
  • saved sessions (/session save, /session attach write-through)
  • exit autosaves and MCP/ACP session mirrors (autosave-*, mcp-*)
  • agent park snapshots (/park, /resume)
  • the transcript journal (line by line) and /memory export files
  • long-term memory JSON stores (facts, episodes, profile, topics, projects, patterns, graph cache, compactor state — daily notes and rollups stay human-editable Markdown)
  • knowledge contexts (~/.chatcli/contexts/*.json; /context export files stay plaintext on purpose)
  • the CCR archive (~/.chatcli/ccr/*.ccr, the originals behind @recall)
  • cost snapshots (~/.chatcli/costs/*.json)
The conversation hub database (SQLite) is the remaining plaintext store; keep it on an encrypted volume.
Format: CHATCLI_ENC_v1 header + 12-byte nonce + AES-256-GCM ciphertext. The key is derived with HKDF-SHA256 from SHA-256(CHATCLI_ENCRYPTION_KEY); the secret itself is never written anywhere.
Transparent migration. Plaintext files written before the key existed keep loading and are re-written encrypted on their next save. An encrypted file opened without the key fails with a clear error naming CHATCLI_ENCRYPTION_KEY — it is never silently treated as empty or corrupt.Persisted redaction and the REPL history. Secret redaction always runs on the LLM path; under CHATCLI_ENV_REDACT_MODE=strict it also masks what ChatCLI persists for itself — session files, the transcript journal, CCR archives, hub mirrors — always on a copy, never the live history (the permissive default keeps stores verbatim so /rewind and exports stay faithful). The redactor covers Slack tokens and webhooks, PEM private keys, connection strings with credentials, AWS secret keys, GCP service-account key ids and Azure account/SAS keys. With the at-rest key set, the REPL prompt history (.chatcli_history) is sealed line by line. Retention re-runs every 6 h in the gateway daemon and expires tenant parks, sessions and queued memory segments past the session window (your own named sessions are never touched).Read-only latch. A memory store whose sealed file this process cannot open (key unset, wrong, or retired without CHATCLI_ENCRYPTION_KEY_PREVIOUS) is locked: it loads empty in memory, logs an error, refuses every write, and is listed under “Locked stores” in /config security and /memory stats. A gateway daemon, a cron job or a shell started without the key can therefore never overwrite your memory. Daily notes and rollups stay plain Markdown by contract; the memory worker’s pending queue is redacted and sealed like the other stores.

Key rotation

  1. Set the new secret in CHATCLI_ENCRYPTION_KEY and list the retired one in CHATCLI_ENCRYPTION_KEY_PREVIOUS (comma-separated when several). Reads try the current key first, then the retired ones; writes always use the current key.
  2. Run /config security reseal: every store file under the state root (sessions, transcripts, memory, contexts, CCR, costs — per tenant under the gateway) is rewritten with the current key; plaintext files get sealed on the way. The command reports how many files changed and the key fingerprint.
  3. Unset CHATCLI_ENCRYPTION_KEY_PREVIOUS.
/config security shows whether encryption is on, the current key’s fingerprint, how many retired keys are configured and what the seal covers.

Tamper-evident audit trail

Every line of the audit trail (CHATCLI_AUDIT_LOG_PATH) carries seq, prev_hash, chain_v and hashhash = SHA-256(prev_hash ‖ canonical sorted-key JSON of the entry), a form independent of which process wrote it. An edited, removed or reordered line breaks the chain from that point on. Several writers share one file safely: every append takes an exclusive file lock, re-reads the tail when the file changed under it (another writer appended, or the file rotated) and only then links the new line — the REPL, a gateway daemon and the gRPC server (kind: "grpc") form one chain. The file rotates at 64 MiB; the first line of the new file names the file it continues (rotated_from) and links to its last hash, so verification follows the boundary, and the retention pass removes rotated files past the session window (the live file is never touched). With encryption at rest enabled every line is sealed on disk (enc: prefix) and opened transparently on verify. A torn last line (a crash mid-write) is reported as such, never as tampering, and the next entry continues from the last complete line. /config security verify-audit [path] re-hashes the trail and reports the first broken line, the sealed count, the rotation origin, a torn tail and the rotated siblings; trails written before the shared chain still verify with their original hash.

TLS 1.3 Transport Security

If TLS certificate loading fails, the error is written to both stderr and the structured log, including the cert and key paths. In containers, this ensures the error is visible via kubectl logs even if the structured logger cannot flush before the crash.

Secret Redaction on the LLM Path

Content the model receives without the user retyping it passes through one redaction chokepoint before it leaves the process: tool outputs in agent/coder mode (file reads, exec, plugins, MCP), squad worker tool outputs, the @file/@git/@env context assembled in chat, and the conversation segment handed to the memory extractor — so a secret pasted into a conversation is neither sent out again nor distilled into a persisted fact. Two layers compose. KEY=VALUE lines (env dumps, .env files, compose/CI logs) are judged by nameAWS_SECRET_ACCESS_KEY, DATABASE_URL, anything ending in _TOKEN, _PASSWORD, _KEY — plus value heuristics (known prefixes, long hex). Free text is scanned for the value shapes providers hand out (sk-…, ghp_…, AKIA…, JWTs, bearer headers, credential fields in JSON).
Humans still see the full output in the terminal and PostToolUse hooks still receive it; only the model’s copy is redacted.

OS Keychain Integration

The key that encrypts stored OAuth credentials (~/.chatcli/auth-profiles.json) can live in the OS keychain instead of a file:
The three backends differ in what happens to a key that already exists on disk:
  • file — the file, always. The keychain is never consulted.
  • keychain — the keychain. A key already on disk is migrated into it once, and the file is removed only after the keychain has handed that key back. A write that appeared to succeed and a read that returned nothing would otherwise leave credentials no future process can decrypt.
  • auto (default) — an existing file key keeps being used, untouched. Only a key being created for the first time goes to the keychain, and only where one is available. Relocating a working installation’s key without being asked is not a default’s business.
Every failure keeps the file: an unreachable keychain, a refused write, a lost write, or a stored value that is not a 32-byte key all leave the on-disk key exactly where it is, with one warning per process. /config server prints the backend actually in effect, which is not always the one requested.
Windows support uses the Credential Manager API directly (CredReadW/CredWriteW/CredDeleteW), because cmdkey can create and list credentials but never reveals a secret. Entries are stored per machine rather than roaming.

Agent Mode Security

Command Allowlist (Strict Mode)

Strict mode is the default. Only commands on the allowlist run — and the rule applies to every command on the line, not just the first one. A line is a sequence of invocations, so checking only the leading word would make any allowed command a passphrase for the rest of it:
Decomposition uses a real shell parser, so quoting, heredocs, subshells and escaped operators are read the way the shell reads them. A line the parser cannot read falls back to checking the leading command only — a host whose shell is not bash would otherwise lose every command — and the denylist below still applies to the whole line. The default allowlist holds around 200 commands:
The allowlist is not a capability list. It includes shell interpreters (sh, bash, zsh), eval, exec, source and rm, because ordinary development work uses them. What actually stops a destructive command is the denylist below, which runs on every line in both modes, and — where you enable it — the coder sandbox.If you need a genuinely restricted surface, do not rely on strict mode alone: run ChatCLI in a container, enable CHATCLI_CODER_SANDBOX, and keep CHATCLI_AGENT_WORKSPACE_STRICT on.

Custom Allowlist

Extend the allowlist with your own commands. Commas and semicolons both work:

Denylist Patterns

The denylist is not limited to permissive mode: it runs on every command in both modes, as the layer that actually refuses destructive work. Around 50 patterns:
Inline interpreter code is classified, not pattern-matched. In agent mode, python -c, perl -e, ruby -e, node -e and php -r are no longer blocked by a regex on the invocation — the inline source is analysed and only high-risk code is refused, so python -c "print(1)" runs and python -c "import os; os.system(...)" does not. The @coder path keeps the stricter regex form and refuses the whole family.

Read Path Blocking

In strict workspace mode, the agent can only read files within the current workspace directory:

Shell Configuration Sourcing

By default, shell configuration files (~/.bashrc, ~/.zshrc) are not sourced during agent command execution to prevent malicious aliases and functions:

Input guard — typeahead protection in security prompts

When a security box appears (coder/agent mode), three layers defend against accidental typing being consumed as a y/n response:
  1. Flush kernel TTYTCIFLUSH (Linux) / TIOCFLUSH (BSD/Darwin) / FlushConsoleInputBuffer (Windows) discards bytes in the kernel queue before the box renders.
  2. Drain channel — empties the centralized non-blocking stdin channel (the 10-line buffer the reader goroutine uses).
  3. Intent debounce — discards any input that arrives in the first 250ms after the box is drawn (minimum human reaction window).
Without these layers, accidentally typing during the LLM stream would let the security box consume the queued bytes as approval. The first time this happened motivated the input guard. Additionally, at the start of every agent turn, ChatCLI runs stty sane on the controlling /dev/tty to recover from a prior go-prompt teardown that may have left the terminal in raw mode (echo off). Without this reset, you type and don’t see characters on screen — even though the kernel is capturing them.

Output Sanitizer

Agent command output is sanitized before being sent to the LLM:
  • ANSI escape sequences are stripped
  • Output is truncated to prevent context overflow

EDITOR Validation

When the user edits commands in agent mode, the EDITOR variable is validated against an allowlist of known editors:
If EDITOR contains an unknown value (e.g., EDITOR="/tmp/exploit.sh"), the operation is refused with an error. The validated editor is then resolved via exec.LookPath to obtain the absolute path.

Kubeconfig Access Control

Control whether agent commands can access kubeconfig:

Shell Injection Protection

All code paths where dynamic values are interpolated into shell commands use the utils.ShellQuote() function, which applies POSIX quoting with single quotes:
This protects against:
  • Quote injection: '; rm -rf /; echo '
  • Command substitution: $(malicious) or `malicious`
  • Variable expansion: $HOME, ${PATH}
  • Pipe/redirection: | cat /etc/passwd, > /etc/crontab

Binary Resolution via LookPath

The stty binary (used to restore the terminal) is resolved once at startup via exec.LookPath("stty"), returning the absolute path. This prevents an attacker from placing a malicious stty in the PATH.

Plugin Security

Ed25519 Signature Verification

Plugin binaries are verified with Ed25519 signatures. A plugin is signed with a developer’s private key, and the matching public key must be registered on every machine that installs it.
1

Generate a signing key pair

Keygen refuses to overwrite an existing private key: regenerating over one silently invalidates every signature made with it.
2

Sign the plugin

3

Register the public key on machines that install it

Without this step a signature is unverifiable: the verifier only reads keys it finds in the trusted directory.
4

Distribute and check

Verify reports which of three things is wrong — no signature, no trusted key to check it against, or a signature that does not match — because they call for different fixes. ChatCLI runs the same check whenever it loads the plugins directory.

What happens to an unsigned plugin

With the default in place and no trusted key registered, no external plugin loads at all. That is the intended posture, and it means adopting plugins is a deliberate act: sign them and register the key, or set CHATCLI_ALLOW_UNSIGNED_PLUGINS=true and accept what that means.

Quarantine for Unsigned Plugins

A newly seen unsigned plugin can be held out of the runtime for a window — the gap between a binary appearing in the plugins directory and that binary running with ChatCLI’s permissions.
/plugin quarantine [release <name>] does the same inside the REPL.
  • It applies only to unsigned plugins, and only where CHATCLI_ALLOW_UNSIGNED_PLUGINS=true already tolerates them. A verified signature is a stronger statement than any waiting period.
  • Replacing a binary restarts its wait. The review was of the bytes, not of the filename.
  • A release records that a human vouched for those exact bytes; state survives a restart, so waiting periods do not reset when ChatCLI starts.
Quarantine is off by default. A delay between installing a plugin and using it is a real cost, and imposing it on everyone to harden a mode that is itself opt-in would trade a certain annoyance for a speculative gain. Turn it on where unsigned plugins are tolerated but unreviewed ones are not.

What plugin sandboxing does not do

There is no per-plugin permission manifest. A plugin is a separate executable that ChatCLI launches, and it runs with the same permissions as ChatCLI itself — the same filesystem, the same network, the same ability to start processes. Nothing constrains an individual plugin to a declared set of capabilities.The controls that do apply are the ones above: a signature says who produced the binary, and quarantine delays an unreviewed one. Neither limits what a plugin does once it runs. Treat installing a plugin as equivalent to running its author’s code on your machine, because that is what it is. Where that is not acceptable, run ChatCLI itself inside a container with the access you are willing to grant.

gRPC Server Security

SSRF Prevention

Provider URLs a client supplies are checked before the server will use them, so a caller cannot point the server at an internal address. The web-fetching tools apply their own dial-time check, including redirects and DNS rebinding. Blocked ranges:
  • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918)
  • 127.0.0.0/8 (loopback)
  • 169.254.0.0/16 (link-local, including cloud metadata endpoints)
  • ::1/128, fc00::/7 (IPv6 private)

Rate Limiting

Token-bucket rate limiting protects against abuse and DoS:

Message Size Limits

Prevent memory exhaustion from oversized messages:
The 50MB default exists because a long-context prompt with history and cache markers passes a few megabytes easily; a 4MB cap would refuse ordinary requests. Lower it wherever the server does not carry long contexts — it is the cheapest bound on memory a caller can force the process to allocate.

Input Validation

Every RPC the service exposes has a validator, and a test walks the generated service descriptor to keep it that way — a new RPC cannot ship without someone deciding how its request is bounded.
  • String and byte-length limits on every text field
  • Repeated fields bounded (conversation history, insight recommendations, metadata maps)
  • Enum validation for severity
  • Kubernetes naming rules (RFC 1123) for namespaces, object names and kinds
  • Numeric ranges for risk scores, step counters and page limits
Streaming RPCs are validated too. A stream interceptor never sees the messages itself, so validation wraps the stream and checks each one as it arrives. That covers the single request of a server-streaming RPC and every message of the bidirectional session — the only path where one connection can keep sending for as long as it stays open.

Audit Logging

All sensitive operations are recorded in structured JSON audit logs:
Example audit log entry:
result distinguishes success, error and denied — a call authentication refused is a security event and does not read like a handler failure. Streaming RPCs produce an entry too, with details.stream and the number of messages received.

LLM request trail (every surface, every provider)

The gRPC audit above records transport metadata. With the same CHATCLI_AUDIT_LOG_PATH, the CLI also records every LLM request on every surface — REPL, one-shot, gateway, MCP/ACP server, squad workers — as kind: "llm" lines in the same file, one on send and one on receive. The sink hangs off the observability chokepoint all fifteen provider adapters pass through, so a new provider is audited the day it ships. A line carries when, provider and model, payload size, history length, cache markers, outcome, latency, the token usage the provider reported and the running total of secrets the LLM-path redactor rewrote in this process — never the prompt content.
The path must be absolute; a relative value disables the trail with a logged error. The file is created 0600 and appended by every process that shares the path.

Bind Address

Control which network interface the server listens on:
In Kubernetes, the bind address is automatically set to 0.0.0.0 — no manual configuration required. The server detects the environment via the KUBERNETES_SERVICE_HOST variable.

Interceptor Chain

All requests pass through a chain of gRPC interceptors:
1

Validation

Bounds every field of the request against the RPC’s validator, before anything else touches it.
2

Rate Limiting

Token-bucket rate limiter per client IP.
3

Audit

Wraps everything inward so it records refusals as well as successes, and names the caller authentication resolves further in.
4

Recovery

Captures panics and returns a gRPC error instead of crashing the server.
5

Logging

Records method, duration and status of each request.
6

Auth

Validates the JWT or bearer token and attaches the caller’s role.
RBAC is not an interceptor: each handler asks for the level it needs, because the answer depends on what the call does rather than on which method was invoked.

gRPC Reflection (Disabled by Default)

gRPC reflection exposes the full service schema, allowing tools like grpcurl and grpcui to discover and call all RPCs. In production, this can facilitate reconnaissance by attackers.
By default, reflection is disabled. Enable only for local debugging.

Kubernetes Operator Security

Fail-Closed Authentication

The operator webhook uses fail-closed authentication: if the authentication service is unavailable, all requests are denied. This prevents unauthorized access during outages. API keys are hot-reloaded every 30 seconds with the following priority order:
  1. Secret chatcli-operator-secrets (priority) — api-keys field containing a YAML list of {key, role, description} entries
  2. ConfigMap chatcli-operator-config (fallback) — same api-keys field
  3. Reject the request (or accept in dev-mode if CHATCLI_OPERATOR_DEV_MODE=true)
Don’t confuse the two Auth Secrets — both typically live in the operator’s namespace:chatcli-operator-secrets must live in the same namespace as the operator pod (the controller calls Secrets(resolveNamespace()).Get(...)resolveNamespace() reads the POD_NAMESPACE env var, the ServiceAccount namespace file, or falls back to the default chatcli-system). If you ran helm install --namespace <X>, create the Secret in <X>.
API keys stored in the Secret are hot-reloaded every 30s — no operator restart is needed.

Resource Type Allowlist

Automated remediation touches only resource types on an allowlist. The default is wider than a single workload type, because remediation that cannot touch a Service or an HPA is remediation that escalates to a human for routine work:
CHATCLI_ALLOWED_RESOURCE_TYPES adds to that list; it does not replace it. Setting it to a short list does not narrow anything — the seventeen defaults stay, and your entries are added on top. Kinds are matched by their exact Kind spelling (Deployment, not deployments), so a lowercase plural adds nothing at all.
To genuinely restrict what the operator may touch, restrict its RBAC: the chart’s Role is namespace-scoped by default, and the operator can only act on what its ServiceAccount is granted.
A second list names types that are never remediated automatically and always require explicit approval, with the reason recorded — ClusterRole and ClusterRoleBinding (cluster-wide escalation), Role, RoleBinding, Namespace, Node, PersistentVolume, StorageClass, Secret, ServiceAccount, NetworkPolicy, the webhook configurations, CustomResourceDefinition and PriorityClass. Anything outside both lists is rejected and logged.

Log Scrubbing

Secrets, tokens, and sensitive data are automatically scrubbed from operator logs:

CORS Policy

The operator REST API is deny-all until an origin is named: with none configured, no CORS headers are written and a browser blocks every cross-origin call.
Or directly:
An allowlist of several origins echoes the request’s own origin after matching it, with Vary: Origin, because the header carries one value and echoing an unmatched origin would turn the list into “any site”. "*" is accepted; combined with credentials it echoes the origin instead, since browsers reject the literal star in that combination. The operator logs which policy took effect at startup.

RBAC and NetworkPolicy

The chart ships a NetworkPolicy, disabled by default. Enabling it restricts ingress to the ports the server actually serves; egress is a separate choice:
egress: restricted narrows outbound traffic to DNS, HTTPS and the Kubernetes API:
Start with egress: allowAll, confirm ingress behaves, then tighten. DNS is included in the narrow form and is not optional: a pod that cannot resolve names fails in ways that look nothing like a network policy problem. If the server calls a private model endpoint or an internal service, add its port to egressExtraPorts before switching.

Pod SecurityContext

The Helm chart defines a restrictive SecurityContext by default:
When securityContext.readOnlyRootFilesystem is true, the chart automatically mounts an emptyDir volume at /tmp (limited to 100Mi) so the application can write temporary files.

Operator Dev Mode

For local development, the operator can run in dev mode with relaxed security:
Never enable CHATCLI_OPERATOR_DEV_MODE in production. It disables critical security checks.

Operator TLS


Container Security (Docker)

The docker-compose.yml includes the following hardening measures:

CI/CD Security

ChatCLI’s CI/CD pipeline includes multiple security checks:
1

govulncheck

Scans Go dependencies for known vulnerabilities using the Go vulnerability database.
2

gosec

Static analysis security scanner for Go code that detects common vulnerabilities.
3

Dependabot

Automated dependency updates with security alerts for vulnerable packages. Configured via .github/dependabot.yml.
4

Cosign Image Signing

Container images are signed using Sigstore Cosign for supply chain verification.

Coder Mode Governance (Policy Manager)

Word Boundary Matching

The policy system uses word boundary matching to prevent permission escalation by prefix. Example: The logic checks whether the next character after the match is a separator (space, /, =, etc.) and not a word continuation (letter, digit, -, _). This ensures that read does not match readlink.

Default Rules

Read commands are allowed; execution always asks:
The policy file is written 0600 at ~/.chatcli/coder_policy.json; a coder_policy.json in the working directory overrides it for that project.

The dangerous-command guard sits below the policy

Every @coder subcommand that runs a shell line — exec and test — is checked against the dangerous-pattern list regardless of what the policy says, including after an “allow always”. A command that matches is refused and the model is told not to retry it.
--allow-unsafe and --allow-sudo exist on both subcommands for the cases that genuinely need them, and they lift only the engine’s own check — the guard above still applies in agent and coder mode.
For more details on the governance system, see the Coder Mode documentation.

Managed Configuration (organization defaults and locked policies)

An operator can ship a managed.env with the machine image or the MDM profile and have every ChatCLI process on that machine honor it — REPL, one-shot, gateway, MCP/ACP server alike: The file is dotenv-shaped with two kinds of lines:
Precedence, highest first: locked managed → user environment / .env → managed default → code default. /config managed shows the file, its entries and which are locked; every /config section tags values that came from it as (managed) or (managed · locked). An unreadable file is reported once at boot and ignored (never a crash); a missing file changes nothing.

Security Environment Variables Reference

Complete reference of all security-related environment variables:

Server Security

Agent Security

Plugin and Auth Security

Operator Security


Version Check

ChatCLI automatically checks for newer versions on GitHub. To disable (e.g., air-gapped environments or CI/CD):

Production Best Practices

1

Use JWT authentication with RBAC

2

Enable TLS in production

3

Use strict agent security mode

4

Require plugin signatures

Keep CHATCLI_ALLOW_UNSIGNED_PLUGINS as false (default), sign your plugins and register the public key:
Where unsigned plugins must be tolerated, set CHATCLI_PLUGIN_QUARANTINE=24h so a binary nobody installed on purpose does not run the moment it appears.
5

Require client certificates

6

Turn on encryption at rest

Verify the audit trail periodically with /config security verify-audit.
7

Sandbox coder execution

8

Configure rate limiting

9

Enable audit logging

10

Keep gRPC reflection disabled

Do not set CHATCLI_GRPC_REFLECTION=true in production. Use only for local debugging.
11

Use namespace-scoped RBAC

Keep rbac.clusterWide: false (default) unless you need to monitor multiple namespaces.
12

Set resource limits

Always define CPU and memory limits to prevent excessive consumption:
13

Enable environment variable redaction

14

Use the OS keychain for the credential key

Check /config server afterwards: it prints the backend that actually took effect, which is the file wherever no keychain is available.
15

Keep ChatCLI updated

The version check is enabled by default. If you disabled it with CHATCLI_DISABLE_VERSION_CHECK, check periodically:

Next Steps

Coder Mode Governance

Policy rules to control what the Coder can execute.

Configure the Server

Deploy and configure the gRPC server.

Deploy with Docker and Helm

Complete containerized deployment guide.

Environment Variables

Complete environment variables reference.

K8s Operator

AIOps autonomous remediation platform.

Plugin System

Extend ChatCLI with custom plugins.