Security Overview
The table below summarizes all active protections across every layer of the stack.Authentication and Authorization
JWT Authentication (Recommended)
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.- RS256 (RSA public key)
- Via Helm
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:viewer and readonly grant the same thing.
/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.
- Via flag
- Via environment variable
OAuth 2.0 + PKCE
ChatCLI supports OAuth 2.0 with PKCE for the following providers: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)
CHATCLI_ENCRYPTION_KEY is set./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 attachwrite-through) - exit autosaves and MCP/ACP session mirrors (
autosave-*,mcp-*) - agent park snapshots (
/park,/resume) - the transcript journal (line by line) and
/memory exportfiles - 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 exportfiles stay plaintext on purpose) - the CCR archive (
~/.chatcli/ccr/*.ccr, the originals behind@recall) - cost snapshots (
~/.chatcli/costs/*.json)
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.
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
- Set the new secret in
CHATCLI_ENCRYPTION_KEYand list the retired one inCHATCLI_ENCRYPTION_KEY_PREVIOUS(comma-separated when several). Reads try the current key first, then the retired ones; writes always use the current key. - 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. - 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 hash — hash = 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
- Server TLS
- Mutual TLS (mTLS)
- Development (no TLS)
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 name — AWS_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).
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:
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.
/config server prints the backend actually in effect, which is not always the one requested.
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:File operations
File operations
Text processing
Text processing
Development tools
Development tools
Containers and infrastructure
Containers and infrastructure
Network
Network
System information
System information
Editors and viewers
Editors and viewers
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: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:- Flush kernel TTY —
TCIFLUSH(Linux) /TIOCFLUSH(BSD/Darwin) /FlushConsoleInputBuffer(Windows) discards bytes in the kernel queue before the box renders. - Drain channel — empties the centralized non-blocking stdin channel (the 10-line buffer the reader goroutine uses).
- Intent debounce — discards any input that arrives in the first 250ms after the box is drawn (minimum human reaction window).
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, theEDITOR variable is validated against an allowlist of known editors:
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 theutils.ShellQuote() function, which applies POSIX quoting with single quotes:
- 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
Thestty 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.Generate a signing key pair
Sign the plugin
Register the public key on machines that install it
Distribute and check
What happens to an unsigned plugin
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=truealready 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.
What plugin sandboxing does not do
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: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
Audit Logging
All sensitive operations are recorded in structured JSON audit logs: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 sameCHATCLI_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.
0600 and appended by every process that shares the path.
Bind Address
Control which network interface the server listens on:Interceptor Chain
All requests pass through a chain of gRPC interceptors:Validation
Rate Limiting
Audit
Recovery
Logging
Auth
gRPC Reflection (Disabled by Default)
gRPC reflection exposes the full service schema, allowing tools likegrpcurl and grpcui to discover and call all RPCs. In production, this can facilitate reconnaissance by attackers.
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:- Secret
chatcli-operator-secrets(priority) —api-keysfield containing a YAML list of{key, role, description}entries - ConfigMap
chatcli-operator-config(fallback) — sameapi-keysfield - Reject the request (or accept in dev-mode if
CHATCLI_OPERATOR_DEV_MODE=true)
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: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.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
- Namespace-Scoped RBAC (Default)
- Cluster-Wide RBAC
egress: restricted narrows outbound traffic to DNS, HTTPS and the Kubernetes API:
Pod SecurityContext
The Helm chart defines a restrictive SecurityContext by default: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:Operator TLS
Container Security (Docker)
Thedocker-compose.yml includes the following hardening measures:
CI/CD Security
ChatCLI’s CI/CD pipeline includes multiple security checks:govulncheck
gosec
Dependabot
.github/dependabot.yml.Cosign Image Signing
Coder Mode Governance (Policy Manager)
Word Boundary Matching
The policy system uses word boundary matching to prevent permission escalation by prefix. Example:/, =, 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: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.Managed Configuration (organization defaults and locked policies)
An operator can ship amanaged.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:
.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
Use JWT authentication with RBAC
Enable TLS in production
Use strict agent security mode
Require plugin signatures
CHATCLI_ALLOW_UNSIGNED_PLUGINS as false (default), sign your plugins and register the public key:CHATCLI_PLUGIN_QUARANTINE=24h so a binary nobody installed on purpose does not run the moment it appears.Require client certificates
Turn on encryption at rest
/config security verify-audit.Sandbox coder execution
Configure rate limiting
Enable audit logging
Keep gRPC reflection disabled
CHATCLI_GRPC_REFLECTION=true in production. Use only for local debugging.Use namespace-scoped RBAC
rbac.clusterWide: false (default) unless you need to monitor multiple namespaces.Set resource limits
Enable environment variable redaction
Use the OS keychain for the credential key
/config server afterwards: it prints the backend that actually took effect, which is the file wherever no keychain is available.Keep ChatCLI updated
CHATCLI_DISABLE_VERSION_CHECK, check periodically: