Skip to content

This page covers Z.E.N.'s security model: how AI permissions work, how platform access is controlled, how webhooks are verified, and what data is and is not logged.

Permission Model

Z.E.N. runs the Claude Code SDK in bypassPermissions mode. This means the provider can read, write, and execute files without interactive confirmation prompts.

Why this is used:

  • Z.E.N. is designed for automated, unattended workflows triggered from Slack, Telegram, GitHub, and other platforms where there is no human at a terminal to approve each action.
  • Requiring interactive permission prompts would block every workflow and make remote operation impossible.

What this means in practice:

  • The provider has full read/write access to the working directory (the cloned repository or worktree).
  • It can run shell commands, modify files, and use all tools available to the Claude Code SDK.
  • There is no per-action confirmation step.

Mitigations:

  • Workflows that touch git can opt into an isolated worktree (worktree.enabled: true), limiting the blast radius of any changes; non-git workflows skip the worktree by default.
  • Workflows support per-node tool restrictions (see below) to constrain what the AI can do at each step.
  • The system is designed as a single-developer tool; there is no multi-tenant isolation.

WARNING

Because bypassPermissions grants full file and shell access, only run Z.E.N. in environments where the provider is trusted with the repository contents. Do not expose Z.E.N. to untrusted users without adapter-level authorization (see below).

Tool Restrictions

Workflow nodes support allowed_tools and denied_tools to restrict which tools the AI can use at each step. This is useful for creating sandboxed steps that can only read code (not modify it) or preventing specific tool usage.

yaml
nodes:
  - id: review
    prompt: "Review the code for security issues"
    allowed_tools: [Read, Grep, Glob]  # Can only read, not write

  - id: implement
    prompt: "Fix the issues found"
    denied_tools: [WebSearch, WebFetch]  # No internet access

How it works:

  • allowed_tools is a whitelist; only listed tools are available. An empty list ([]) disables all tools.
  • denied_tools is a blacklist; listed tools are blocked, all others are available.
  • These are mutually exclusive per node. If both are set, allowed_tools takes precedence.
  • Tool restrictions are currently supported for the Claude provider only. Codex nodes with denied_tools will log a warning; allowed_tools is not supported by the Codex SDK.

Data Privacy and Logging

Z.E.N. uses structured logging (Pino) with explicit rules about what is and is not recorded.

Never logged:

  • API keys or tokens (masked to first 8 characters + ... when referenced)
  • User message content (the text users send to the AI)
  • Personally identifiable information (PII)

Logged (with context):

  • Conversation IDs, session IDs, workflow run IDs
  • Event names (e.g., session.create_started, workflow.step_completed)
  • Error messages and types (for debugging)
  • Unauthorized access attempts (with masked user IDs, e.g., abc***)

Log levels:

  • Default: info (operational events only)
  • Set LOG_LEVEL=debug for detailed execution traces
  • CLI: --quiet (errors only) or --verbose (debug)

Adapter Authorization

Each platform adapter supports an optional user whitelist via environment variables. When a whitelist is configured, only listed users can interact with the bot. When the whitelist is empty or unset, the adapter operates in open access mode.

PlatformWhitelist VariableFormat
SlackSLACK_ALLOWED_USER_IDSComma-separated Slack user IDs (e.g., U01ABC,U02DEF)
TelegramTELEGRAM_ALLOWED_USER_IDSComma-separated Telegram user IDs
GitHubGITHUB_ALLOWED_USERSComma-separated GitHub usernames (case-insensitive)

Authorization behavior:

  • Whitelist is parsed once at adapter startup (from the environment variable).
  • Every incoming message or webhook is checked before processing.
  • Unauthorized users are silently rejected; no error response is sent back.
  • Unauthorized attempts are logged with masked user identifiers for auditing.
  • The Web UI has no built-in user authentication. Use CADDY_BASIC_AUTH or form auth when exposing it publicly (see Docker / Deployment variables).

Webhook Security

The GitHub adapter verifies webhook signatures to ensure payloads originate from GitHub and have not been tampered with.

GitHub:

  • Uses the X-Hub-Signature-256 header
  • HMAC SHA-256 computed over the raw request body using WEBHOOK_SECRET
  • Timing-safe comparison prevents timing attacks
  • Invalid signatures are rejected and logged

Setup:

  1. Generate a random secret: openssl rand -hex 32
  2. Set it in both the GitHub webhook configuration and Z.E.N.'s environment (WEBHOOK_SECRET)
  3. The secrets must match exactly

Secrets Handling

Environment files:

  • All secrets (API keys, tokens, webhook secrets) belong in .env files, never in source control.
  • The .env.example file in the repository contains placeholder values; copy it and fill in real values.
  • Never commit .env files to git. The repository's .gitignore excludes them.

Subprocess env isolation:

  • Bun auto-loads .env from CWD before any Z.E.N. code runs. These vars remain in the server/CLI's process.env but cannot reach AI subprocesses; Claude Code subprocesses receive only an explicit allowlist of env vars (SUBPROCESS_ENV_ALLOWLIST: system essentials, Claude auth, Z.E.N. runtime config, git identity, GitHub tokens). Keys like ANTHROPIC_API_KEY, OPENAI_API_KEY, and DATABASE_URL are not on the allowlist and are blocked.
  • ~/.zen/.env is loaded with override: true, so Z.E.N.'s own config always wins over any Bun-auto-loaded CWD vars for overlapping keys.
  • Per-codebase env vars configured via codebase_env_vars or .zen/config.yaml env: are merged on top of this filtered base at workflow execution time.

Env-leak gate (target repo .env keys)

Beyond the subprocess allowlist, Z.E.N. also scans target repos for sensitive keys before spawning. A Claude or Codex subprocess started with cwd=/path/to/target/repo inherits its own Bun auto-loaded .env; the env-leak gate catches this by scanning the target repo's .env files at registration and pre-spawn time.

What Z.E.N. scans: auto-loaded filenames .env, .env.local, .env.development, .env.production, .env.development.local, .env.production.local.

Scanned keys: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, OPENAI_API_KEY, CODEX_API_KEY, GEMINI_API_KEY.

WARNING

Renaming the file to .env.local, .env.development, etc. does not work; Bun auto-loads those too. Only .env.secrets (or any non-auto-loaded name) is safe.

Where the gate runs:

Failure pointWhenWhat you see
Registration (Web UI)Adding a project via Settings → Add Project422 with the "Allow env keys" checkbox shown inline
Registration (CLI)First zen workflow run --cwd <repo> auto-registersError message points at --allow-env-keys and the global config flag
Pre-spawnExisting codebase, before each Claude/Codex queryError message points at Settings → Projects → "Allow env keys" toggle

Primary remediation (recommended):

  1. Remove the key from the target repo's .env, or
  2. Rename the file to .env.secrets and load it explicitly from your app code.

Secondary remediation (consent grants):

  • Web UI: Settings → Projects → click "Allow env keys" on the row. Revoke from the same place. Each grant/revoke writes a warn-level audit log (env_leak_consent_granted / env_leak_consent_revoked) including codebaseId, path, scanned files, matched keys, scanStatus ('ok' or 'skipped'), and actor.
  • CLI: zen workflow run <name> "your message" --cwd <repo> --allow-env-keys grants consent during this run's auto-registration. The grant is persisted (the codebase row is created with allow_env_keys = true) and logged as env_leak_consent_granted with actor: 'user-cli'.
  • Global bypass: set allow_target_repo_keys: true in ~/.zen/config.yaml to disable the gate for all codebases on this machine. env_leak_gate_disabled is logged at most once per process per source (global vs. repo) the first time loadConfig resolves the bypass as active. A repo-level .zen/config.yaml with allow_target_repo_keys: false re-enables the gate for that repo.

Startup scan: When allow_target_repo_keys is not set, the server scans every registered codebase with allow_env_keys = false and emits one startup_env_leak_gate_will_block warning per codebase that has findings (i.e. would actually be blocked). This gives you a chance to grant consent before hitting a fatal error mid-workflow. The scan is skipped entirely when the global bypass is active.

CORS:

  • API routes use WEB_UI_ORIGIN to restrict CORS. The default is * (allow all), which is appropriate for local single-developer use. Set a specific origin when exposing the server publicly.

Docker deployments:

  • CLAUDE_USE_GLOBAL_AUTH=true does not work in Docker (no local claude CLI). Provide CLAUDE_CODE_OAUTH_TOKEN or CLAUDE_API_KEY explicitly.
  • Escape $ as $$ in Docker Compose .env files to prevent variable substitution of bcrypt hashes.

AI that follows a recipe, not a conversation.