Appearance
Configuration
Z.E.N. tries hard to need no configuration at all. Install it, fire a workflow, and you'll never open a config file. The settings exist for the moments you want to change something: pick a different default provider, point Z.E.N. at a Postgres database instead of the local SQLite, restrict the Slack bot to a specific user, or run two projects with different defaults.
Three layers, in increasing order of specificity: defaults baked into the binary, a YAML file in ~/.zen/config.yaml (your machine), and a YAML file in .zen/config.yaml (this project). Environment variables override all three. The lower layers fill in what the higher ones don't set; you only write the keys you want to change.
Advanced: full configuration reference
Z.E.N. supports a layered configuration system with sensible defaults, optional YAML config files, and environment variable overrides. For a quick introduction, see Getting Started: Configuration.
Directory Structure
User-Level (~/.zen/)
~/.zen/
├── workspaces/owner/repo/ # Project-centric layout
│ ├── source/ # Clone or symlink -> local path
│ ├── worktrees/ # Git worktrees for this project
│ ├── artifacts/ # Workflow artifacts
│ └── logs/ # Workflow execution logs
├── zen.db # SQLite database (when DATABASE_URL not set)
└── config.yaml # Global configuration (optional)Repository-Level (.zen/)
.zen/
├── commands/ # Custom commands
│ └── plan.md
├── workflows/ # Workflow definitions (YAML files)
└── config.yaml # Repo-specific configuration (optional)Configuration Priority
Settings are loaded in this order (later overrides earlier):
- Defaults - Sensible built-in defaults
- Global Config -
~/.zen/config.yaml - Repo Config -
.zen/config.yamlin repository - Environment Variables - Always highest priority
Global Configuration
Create ~/.zen/config.yaml for user-wide preferences:
yaml
# Default provider
defaultAssistant: claude # or 'codex'
# Assistant defaults
assistants:
claude:
model: sonnet
settingSources: # Which CLAUDE.md files the SDK loads (default: ['project'])
- project # Project-level CLAUDE.md (always recommended)
- user # Also load ~/.claude/CLAUDE.md (global preferences)
codex:
model: gpt-5.3-codex
modelReasoningEffort: medium
webSearchMode: disabled
additionalDirectories:
- /absolute/path/to/other/repo
# Streaming preferences per platform
streaming:
telegram: stream # 'stream' or 'batch'
slack: batch
github: batch
# Custom paths (usually not needed)
paths:
workspaces: ~/.zen/workspaces
worktrees: ~/.zen/worktrees
# Concurrency limits
concurrency:
maxConversations: 10
# Env-leak gate bypass (last resort; weakens a security control)
# allow_target_repo_keys: false # Set true to skip the env-leak-gate
# globally for all codebases on this machine.
# `env_leak_gate_disabled` is logged once per
# process per source. See security.md.Repository Configuration
Create .zen/config.yaml in any repository for project-specific settings:
yaml
# provider for this project (used as default provider for workflows)
assistant: claude
# Assistant defaults (override global)
assistants:
claude:
model: sonnet
settingSources: # Override global settingSources for this repo
- project
codex:
model: gpt-5.3-codex
webSearchMode: live
# Commands configuration
commands:
folder: .zen/commands
autoLoad: true
# Worktree settings
worktree:
baseBranch: main # Optional: auto-detected from git when not set
copyFiles: # Optional: Additional files to copy to worktrees
- .env.example -> .env # Rename during copy
- .vscode # Copy entire directory
# Documentation directory
docs:
path: docs # Optional: default is docs/
# Defaults configuration
defaults:
loadDefaultCommands: true # Load app's bundled default commands at runtime
loadDefaultWorkflows: true # Load app's bundled default workflows at runtime
# Concurrency policy (opt-in; default off)
concurrency:
allowParallelWorkflows: false # When true, different workflow names can run
# in parallel on the same cwd. The dispatch
# lock scopes by (working_path, workflow_name)
# instead of working_path alone. Same-name
# back-to-back dispatches still block
# themselves. Useful for fleets that share a
# cwd with `worktree.enabled: false`.
# Default subprocess timeouts for bash/script nodes (ms).
# Per-node `timeout:` in the workflow yaml always overrides these.
# Values must be positive integers ≤ 2147483647 (≈24.8 days); invalid values
# are ignored and the built-in 2-minute default applies.
timeouts:
bash: 600000 # 10 minutes for bash nodes
script: 900000 # 15 minutes for script nodes
# Per-project environment variables for workflow execution (Claude SDK only)
# Injected into the Claude subprocess env. Use the Web UI Settings panel for secrets.
# env:
# MY_API_KEY: value
# CUSTOM_ENDPOINT: https://...
# Per-repo override for the env-leak-gate bypass.
# Set to `false` to re-enable the gate for THIS repo even when the global
# config has `allow_target_repo_keys: true`. Set to `true` to grant the
# bypass for THIS repo only. Wins over the global flag in either direction.
# allow_target_repo_keys: falseClaude settingSources
Controls which CLAUDE.md files the Claude Agent SDK loads during sessions:
| Value | Description |
|---|---|
project | Load the project's CLAUDE.md (default, always included) |
user | Also load ~/.claude/CLAUDE.md (user's global preferences) |
Default: ['project']; only project-level instructions are loaded.
Set in global or repo config:
yaml
assistants:
claude:
settingSources:
- project
- userThis is useful when you maintain coding style or identity preferences in ~/.claude/CLAUDE.md and want Z.E.N. sessions to respect them.
Default behavior: When a workflow opts into a worktree via worktree.enabled: true, the .zen/ directory is copied automatically (artifacts, plans, workflows). Use copyFiles only for additional files like .env or .vscode.
Defaults behavior: The app's bundled default commands and workflows are loaded at runtime and merged with repo-specific ones. Repo commands/workflows override app defaults by name. Set defaults.loadDefaultCommands: false or defaults.loadDefaultWorkflows: false to disable runtime loading.
Base branch behavior: Before creating a worktree, the canonical workspace is synced to the latest code. Resolution order:
- If
worktree.baseBranchis set: Uses the configured branch. Fails with an error if the branch doesn't exist on remote (no silent fallback). - If omitted: Auto-detects the default branch via
git remote show origin. Works without any config for standard repos. - If auto-detection fails and a workflow references
$BASE_BRANCH: Fails with an error explaining the resolution chain.
Docs path behavior: The docs.path setting controls where the $DOCS_DIR variable points. When not configured, $DOCS_DIR defaults to docs/. Unlike $BASE_BRANCH, this variable always has a safe default and never throws an error. Configure it when your documentation lives outside the standard docs/ directory (e.g., packages/docs).
Environment Variables
Environment variables override all other configuration. They are organized by category below.
Core
| Variable | Description | Default |
|---|---|---|
ZEN_HOME | Base directory for all Z.E.N.-managed files | ~/.zen |
PORT | HTTP server listen port | 3090 (auto-allocated in worktrees) |
LOG_LEVEL | Logging verbosity (fatal, error, warn, info, debug, trace) | info |
BOT_DISPLAY_NAME | Bot name shown in batch-mode "starting" messages | Z.E.N. |
DEFAULT_AI_ASSISTANT | Default provider (claude or codex) | claude |
MAX_CONCURRENT_CONVERSATIONS | Maximum concurrent AI conversations | 10 |
SESSION_RETENTION_DAYS | Delete inactive sessions older than N days | 30 |
AI Providers; Claude
| Variable | Description | Default |
|---|---|---|
CLAUDE_USE_GLOBAL_AUTH | Use global auth from claude /login (true/false) | Auto-detect |
CLAUDE_CODE_OAUTH_TOKEN | Explicit OAuth token (alternative to global auth) | ; |
CLAUDE_API_KEY | Explicit API key (alternative to global auth) | ; |
TITLE_GENERATION_MODEL | Lightweight model for generating conversation titles | SDK default |
ZEN_CLAUDE_FIRST_EVENT_TIMEOUT_MS | Timeout (ms) before Claude subprocess is considered hung (throws with diagnostic log) | 60000 |
When CLAUDE_USE_GLOBAL_AUTH is unset, Z.E.N. auto-detects: it uses explicit tokens if present, otherwise falls back to global auth.
AI Providers; Codex
| Variable | Description | Default |
|---|---|---|
CODEX_ID_TOKEN | Codex ID token (from ~/.codex/auth.json) | ; |
CODEX_ACCESS_TOKEN | Codex access token | ; |
CODEX_REFRESH_TOKEN | Codex refresh token | ; |
CODEX_ACCOUNT_ID | Codex account ID | ; |
Platform Adapters; Slack
| Variable | Description | Default |
|---|---|---|
SLACK_BOT_TOKEN | Slack bot token (xoxb-...) | ; |
SLACK_APP_TOKEN | Slack app-level token for Socket Mode (xapp-...) | ; |
SLACK_ALLOWED_USER_IDS | Comma-separated Slack user IDs for whitelist | Open access |
SLACK_STREAMING_MODE | Streaming mode (stream or batch) | batch |
Platform Adapters; Telegram
| Variable | Description | Default |
|---|---|---|
TELEGRAM_BOT_TOKEN | Telegram bot token from @BotFather | ; |
TELEGRAM_ALLOWED_USER_IDS | Comma-separated Telegram user IDs for whitelist | Open access |
TELEGRAM_STREAMING_MODE | Streaming mode (stream or batch) | stream |
Platform Adapters; GitHub
| Variable | Description | Default |
|---|---|---|
GITHUB_TOKEN | GitHub personal access token (also used by gh CLI) | ; |
GH_TOKEN | Alias for GITHUB_TOKEN (used by GitHub CLI) | ; |
WEBHOOK_SECRET | HMAC SHA-256 secret for GitHub webhook signature verification | ; |
GITHUB_ALLOWED_USERS | Comma-separated GitHub usernames for whitelist (case-insensitive) | Open access |
GITHUB_BOT_MENTION | @mention name the bot responds to in issues/PRs | Falls back to BOT_DISPLAY_NAME |
Database
| Variable | Description | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string (omit to use SQLite) | SQLite at ~/.zen/zen.db |
Web UI
| Variable | Description | Default |
|---|---|---|
WEB_UI_ORIGIN | CORS origin for API routes (restrict when exposing publicly) | * (allow all) |
WEB_UI_DEV | When set, skip serving static frontend (Vite dev server used instead) | ; |
Worktree Management
| Variable | Description | Default |
|---|---|---|
STALE_THRESHOLD_DAYS | Days before an inactive worktree is considered stale | 14 |
MAX_WORKTREES_PER_CODEBASE | Max worktrees per codebase before auto-cleanup | 25 |
CLEANUP_INTERVAL_HOURS | How often the background cleanup service runs | 6 |
Docker / Deployment
| Variable | Description | Default |
|---|---|---|
ZEN_DATA | Host path for Z.E.N. data (workspaces, worktrees, artifacts) | Docker-managed volume |
DOMAIN | Public domain for Caddy reverse proxy (TLS auto-provisioned) | ; |
CADDY_BASIC_AUTH | Caddy basicauth directive to protect Web UI and API | Disabled |
AUTH_USERNAME | Username for form-based auth (Caddy forward_auth) | ; |
AUTH_PASSWORD_HASH | Bcrypt hash for form-based auth password (escape $ as $$ in Compose) | ; |
COOKIE_SECRET | 64-hex-char secret for auth session cookies | ; |
AUTH_SERVICE_PORT | Port for the auth service container | 9000 |
COOKIE_MAX_AGE | Auth cookie lifetime in seconds | 86400 |
.env File Locations
Infrastructure configuration (database URL, platform tokens) is stored in .env files:
| Component | Location | Purpose |
|---|---|---|
| CLI | ~/.zen/.env | Global infrastructure config; CWD .env keys stripped before loading (no override needed) |
| Server (dev) | <zen-repo>/.env + ~/.zen/.env | Repo .env for platform tokens; ~/.zen/.env loaded with override: true |
| Server (binary) | ~/.zen/.env | Single source of truth (repo .env path is not available in compiled binaries) |
How it works: At startup, the CLI strips all keys that Bun auto-loaded from the current working directory (from .env, .env.local, .env.development, .env.production) before loading ~/.zen/.env. This ensures CWD repo keys are fully removed rather than merely overridden. Target repo env vars cannot reach AI subprocesses; SUBPROCESS_ENV_ALLOWLIST blocks all non-whitelisted keys.
Best practice: Use ~/.zen/.env as the single source of truth:
bash
# Create global config
mkdir -p ~/.zen
cp .env.example ~/.zen/.env
# Edit with your valuesDocker Configuration
In Docker containers, paths are automatically set:
/.zen/
├── workspaces/owner/repo/
│ ├── source/
│ ├── worktrees/
│ ├── artifacts/
│ └── logs/
└── zen.dbEnvironment variables still work and override defaults.
Command Folder Detection
When cloning or switching repositories, Z.E.N. looks for commands in this priority order:
.zen/commands/- Always searched first- Configured folder from
commands.folderin.zen/config.yaml(if specified)
Example .zen/config.yaml:
yaml
commands:
folder: .claude/commands/zen # Additional folder to search
autoLoad: trueExamples
Minimal Setup (Using Defaults)
No configuration needed. Z.E.N. works out of the box with:
~/.zen/for all managed files- Claude as default provider
- Platform-appropriate streaming modes
Custom AI Preference
yaml
# ~/.zen/config.yaml
defaultAssistant: codexProject-Specific Settings
yaml
# .zen/config.yaml in your repo
assistant: claude # Workflows inherit this provider unless they specify their own
commands:
autoLoad: trueCustom storage path
Set ZEN_HOME to relocate workspaces, the database, and config to a different directory. Beta users running Docker get the full container reference as part of their onboarding kit.
Streaming Modes
Each platform adapter supports two streaming modes, configured via environment variable or ~/.zen/config.yaml.
Stream Mode
Messages are sent in real-time as the AI generates responses.
ini
TELEGRAM_STREAMING_MODE=stream
SLACK_STREAMING_MODE=streamPros:
- Real-time feedback and progress indication
- More interactive and engaging
- See AI reasoning as it works
Cons:
- More API calls to platform
- May hit rate limits with very long responses
- Creates many messages/comments
Best for: Interactive chat platforms (Telegram)
Batch Mode
Only the final summary message is sent after AI completes processing.
ini
TELEGRAM_STREAMING_MODE=batch
SLACK_STREAMING_MODE=batchPros:
- Single coherent message/comment
- Fewer API calls
- No spam or clutter
Cons:
- No progress indication during processing
- Longer wait for first response
- Can't see intermediate steps
Best for: Issue trackers and async platforms (GitHub)
Platform Defaults
| Platform | Default Mode |
|---|---|
| Telegram | stream |
| Slack | batch |
| GitHub | batch |
| Web UI | SSE streaming (always real-time, not configurable) |
Concurrency Settings
Control how many conversations the system processes simultaneously:
ini
MAX_CONCURRENT_CONVERSATIONS=10 # Default: 10How it works:
- Conversations are processed with a lock manager
- If the max concurrent limit is reached, new messages are queued
- Prevents resource exhaustion and API rate limits
- Each conversation maintains its own independent context
Tuning guidance:
| Resources | Recommended Setting |
|---|---|
| Low resources | 3-5 |
| Standard | 10 (default) |
| High resources | 20-30 (monitor API limits) |
Health Check Endpoints
The application exposes health check endpoints for monitoring:
Basic Health Check:
bash
curl http://localhost:3090/healthReturns: {"status":"ok"}
Database Connectivity:
bash
curl http://localhost:3090/health/dbReturns: {"status":"ok","database":"connected"}
Concurrency Status:
bash
curl http://localhost:3090/health/concurrencyReturns: {"status":"ok","active":0,"queued":0,"maxConcurrent":10}
Use cases:
- Docker healthcheck configuration
- Load balancer health checks
- Monitoring and alerting systems (Prometheus, Datadog, etc.)
- CI/CD deployment verification
Troubleshooting
Config Parse Errors
If your config file has invalid YAML syntax, you'll see error messages like:
[Config] Failed to parse global config at ~/.zen/config.yaml: <error details>
[Config] Using default configuration. Please fix the YAML syntax in your config file.Common YAML syntax issues:
- Incorrect indentation (use spaces, not tabs)
- Missing colons after keys
- Unquoted values with special characters
The application will continue running with default settings until the config file is fixed.