Skip to content

Configuration reference

Every environment variable the app reads at startup, grouped by concern. The authoritative source is src/config.ts — values are validated via Zod at boot and the process exits if a required variable is missing or malformed.

Default is the fallback when the variable is unset (blank means "no default — must be set when required"). Required when is the runtime condition under which the variable is mandatory.

GitHub App credentials

Server mode only. If ORCHESTRATOR_URL is set, the process runs in daemon mode and these are not required.

Variable Default Required when Notes
GITHUB_APP_ID Server mode Numeric App ID from the App settings page.
GITHUB_APP_PRIVATE_KEY Server mode Full PEM. Literal \n sequences are normalised to real newlines.
GITHUB_WEBHOOK_SECRET Server mode HMAC-SHA256 secret configured in the App settings.
GITHUB_PERSONAL_ACCESS_TOKEN Optional Override App installation token with a PAT — bot acts as the PAT owner. Requires single-owner ALLOWED_OWNERS.

AI provider

Variable Default Required when Notes
CLAUDE_PROVIDER anthropic anthropic or bedrock.
CLAUDE_MODEL claude-opus-4-7 (anthropic); — (bedrock) Bedrock Bedrock requires an explicit Bedrock model ID.
ANTHROPIC_API_KEY Anthropic, unless CLAUDE_CODE_OAUTH_TOKEN is set Console pay-as-you-go. Safe for multi-tenant deploys.
CLAUDE_CODE_OAUTH_TOKEN Anthropic, unless ANTHROPIC_API_KEY is set Max/Pro subscription token (sk-ant-oat…). Requires ALLOWED_OWNERS.
AWS_REGION Bedrock Resolved by the AWS SDK credential chain.
AWS_PROFILE Optional (bedrock) Local SSO profile for dev.
AWS_ACCESS_KEY_ID Optional (bedrock) Long-lived credential pair. Prefer profile or OIDC.
AWS_SECRET_ACCESS_KEY Optional (bedrock) Paired with AWS_ACCESS_KEY_ID.
AWS_SESSION_TOKEN Optional (bedrock) Temporary credentials.
AWS_BEARER_TOKEN_BEDROCK Optional (bedrock, CI) Set automatically by aws-actions/configure-aws-credentials OIDC.
ANTHROPIC_BEDROCK_BASE_URL Optional (bedrock) Override Bedrock runtime endpoint (VPC endpoint / proxy).
ALLOWED_OWNERS OAuth or PAT path Comma-separated allowlist. Required (single owner) when using CLAUDE_CODE_OAUTH_TOKEN or GITHUB_PERSONAL_ACCESS_TOKEN.

HTTP server

Variable Default Notes
PORT 3000 HTTP webhook listener.
LOG_LEVEL info Pino level: fatal, error, warn, info, debug, trace. debug surfaces full webhook payloads.
NODE_ENV production production, development, test.
TRIGGER_PHRASE @chrisleekr-bot Mention text that triggers the bot. Local dev typically sets @chrisleekr-bot-dev.
BOT_APP_LOGIN chrisleekr-bot[bot] Bot's GitHub login. Used by the loop-prevention check.
MAX_CONCURRENT_REQUESTS 3 Ceiling on simultaneous Claude executions across the fleet.
MAX_FETCHED_COMMENTS 500 Per-PR/issue cap on comments merged from the GraphQL fetcher (src/core/fetcher.ts). When the cap fires the fetcher emits log.warn({ connection: "comments", … }) and sets FetchedData.truncated.comments=true.
MAX_FETCHED_REVIEWS 500 Per-PR cap on reviews merged from the fetcher. Sets FetchedData.truncated.reviews=true on cap fire.
MAX_FETCHED_REVIEW_COMMENTS 500 Per-PR cap on inline review comments merged across all reviews (top-level + nested follow-up paginate). Sets truncated.reviewComments=true.
MAX_FETCHED_FILES 500 Per-PR cap on changed files merged from the fetcher. Sets truncated.changedFiles=true on cap fire.
AGENT_TIMEOUT_MS 3600000 Wall-clock budget for one agent execution (60 min). Lower only when the job is bounded.
AGENT_MAX_TURNS unset Optional Claude SDK turn cap. Unset = no cap. Overrides DEFAULT_MAXTURNS.
DEFAULT_MAXTURNS unset Process-wide turn cap. Set only if ops needs a hard ceiling.
CLAUDE_CODE_PATH resolved from node_modules Absolute path to the Claude Code CLI cli.js.
CLONE_BASE_DIR /tmp/bot-workspaces Parent directory for per-delivery clones.
CLONE_DEPTH 50 Shallow-clone depth. Increase for deeply-diverged PRs.
CONTEXT7_API_KEY unset Lifts Context7 MCP rate limiting. No other effect.

Postgres

Required whenever the orchestrator role is active.

Variable Default Notes
DATABASE_URL Postgres connection. Backs executions, triage_results, workflow_runs, ship_intents, ship_iterations, ship_continuations, ship_fix_attempts, repo_memory, daemons.

Valkey

Required whenever the orchestrator role is active.

Variable Default Notes
VALKEY_URL Backs the daemon job queue, in-flight set, the ephemeral-spawn cooldown, the ship:tickle sorted set, and ship cancel flags.

Orchestrator and daemon

Variable Default Notes
WS_PORT 3002 Orchestrator WebSocket listener. Must differ from PORT.
ORCHESTRATOR_URL Presence flips the process to daemon mode. Use wss:// in production; ws:// emits a warning.
ORCHESTRATOR_PUBLIC_URL Public WebSocket URL the spawner injects into ephemeral Pods.
DAEMON_AUTH_TOKEN Shared secret for the daemon ⇄ orchestrator handshake. Required on both sides. Compared in constant time.
DAEMON_AUTH_TOKEN_PREVIOUS Optional rotation overlap. Orchestrator accepts either the primary or this previous token; daemons always send the primary. See runbooks/daemon-fleet.md.
HEARTBEAT_INTERVAL_MS 30000 Daemon → orchestrator ping cadence.
HEARTBEAT_TIMEOUT_MS 90000 Eviction threshold. Keep ≥ 2 × HEARTBEAT_INTERVAL_MS.
STALE_EXECUTION_THRESHOLD_MS 3600000 How long a running execution may sit before the watcher fails it. Set ≥ AGENT_TIMEOUT_MS.
DAEMON_DRAIN_TIMEOUT_MS 300000 Post-SIGTERM window to finish in-flight work. Raise to ≥ AGENT_TIMEOUT_MS for zero mid-run kills.
JOB_MAX_RETRIES 3 Retries for transient daemon dispatch failures.
OFFER_TIMEOUT_MS 5000 How long the orchestrator waits for a daemon to claim an offer.
QUEUE_WORKER_BACKOFF_MAX_MS 5000 Upper bound on the queue-worker's sleep when no local daemon can take a job.
LIVENESS_REAPER_INTERVAL_MS 30000 (min 20000) Cadence of the heartbeat-based reaper.
DAEMON_UPDATE_STRATEGY exit exit, pull, or notify. Advisory hint reported in the update response.
DAEMON_UPDATE_DELAY_MS 0 Delay before graceful shutdown after an update signal.
DAEMON_MEMORY_FLOOR_MB 512 Minimum free memory the orchestrator requires before dispatching.
DAEMON_DISK_FLOOR_MB 1024 Minimum free disk the orchestrator requires before dispatching.

Ephemeral daemons

Used when the orchestrator scales daemon capacity on demand.

Variable Default Notes
DAEMON_EPHEMERAL false Set to true on ephemeral daemon Pods (injected by the spawner). Controls idle-exit.
EPHEMERAL_DAEMON_IDLE_TIMEOUT_MS 120000 Ephemeral daemon exits after this idle window.
EPHEMERAL_DAEMON_SPAWN_COOLDOWN_MS 30000 Minimum time between ephemeral spawns (orchestrator side).
EPHEMERAL_DAEMON_SPAWN_QUEUE_THRESHOLD 3 Queue length that triggers an ephemeral-daemon-overflow spawn.
EPHEMERAL_DAEMON_NAMESPACE default Kubernetes namespace for spawned ephemeral Pods.
DAEMON_IMAGE auto-detected K8s image URI override.
KUBECONFIG auto (in-cluster) Kubernetes client config path. The client auto-detects in-cluster via KUBERNETES_SERVICE_HOST.

The orchestrator also expects a pre-existing daemon-secrets Kubernetes Secret in EPHEMERAL_DAEMON_NAMESPACE, mounted into the spawned Pod via envFrom: secretRef: daemon-secrets. See deployment.md.

Triage

Variable Default Notes
TRIAGE_ENABLED true Kill-switch. When false, triage returns heavy=false and the job routes to persistent-daemon.
TRIAGE_MODEL haiku-3-5 Alias resolved at runtime.
TRIAGE_CONFIDENCE_THRESHOLD 1.0 Below this, triage is treated as sub-threshold and the job routes to persistent-daemon.
TRIAGE_MAX_TOKENS 256 Cap on the JSON response. Above ~100 is wasted budget.
TRIAGE_TIMEOUT_MS 5000 Per-call wall clock. Beyond this, the circuit-breaker counter increments.
INTENT_CONFIDENCE_THRESHOLD 0.75 Range [0, 1]. Below this, a mention-driven comment gets a clarification reply instead of a dispatch.

Ship

Variable Default Notes
MAX_WALL_CLOCK_PER_SHIP_RUN 4h Hard ceiling on a single intent's wall-clock budget. Accepts ms or Nh / Nm / Ns. Per-invocation --deadline is clamped to this.
MAX_SHIP_ITERATIONS 50 Iteration cap. Firing transitions the intent to terminal human_took_over with terminal_blocker_category='iteration-cap'.
CRON_TICKLE_INTERVAL_MS 30000 How often the cron tickle scans ship:tickle for due intents.
MERGEABLE_NULL_BACKOFF_MS_LIST 500,1500,4500 Comma-separated bounded backoff schedule used by the probe when mergeable=null. Exhaustion yields mergeable_pending and the session yields.
REVIEW_BARRIER_SAFETY_MARGIN_MS 1200000 (20 min) Minimum elapsed time since the last bot push before the bot may declare ready without a non-bot review on the current head SHA.
FIX_ATTEMPTS_PER_SIGNATURE_CAP 3 Max attempts per failure signature within a single intent. Cap firing terminates with terminal_blocker_category='flake-cap'.
SHIP_FORBIDDEN_TARGET_BRANCHES empty Comma-separated branches the bot refuses to shepherd PRs against.

Mode matrix — what's required when

Role Required
Orchestrator (webhook server) GitHub App credentials, one AI provider credential, VALKEY_URL, DATABASE_URL, DAEMON_AUTH_TOKEN.
Ephemeral-daemon scale-up K8s API access + RBAC on pods in EPHEMERAL_DAEMON_NAMESPACE, daemon-secrets Secret.
Daemon process (ORCHESTRATOR_URL set) DAEMON_AUTH_TOKEN, one AI provider credential. GitHub App credentials and data-layer URLs are NOT required.

LLM-based output scanner (defense layer 4)

Per-call LLM scan of every agent-generated GitHub-bound body, after the deterministic regex pass in redactSecrets(). Catches encoded / obfuscated secrets the regex misses.

Variable Default Notes
LLM_OUTPUT_SCANNER_ENABLED true Set false to disable. Skipping the scan saves ~1–2s and ~$0.0002 per agent reply but loses the encoded-secret backstop.
LLM_OUTPUT_SCANNER_MODEL haiku-3-5 Operator-friendly alias resolved by src/ai/llm-client.ts MODEL_MAP. Cheapest Haiku that emits the structured JSON schema is sufficient.
LLM_OUTPUT_SCANNER_TIMEOUT_MS 3000 Per-call wall-clock cap. On timeout, the helper FAILS OPEN — posts the body that survived the regex pass and emits a warn log.

System messages (router capacity, marker comments, lifecycle pings) skip the LLM pass — they cannot legitimately contain secrets and the scan is wasted spend.

Subprocess env allowlist (defense layer 1a, issue #102)

The Claude Agent SDK CLI subprocess receives an explicit env allowlist — NOT the full process.env. This eliminates the prompt-injection exfiltration path where a successful injection on the agent could cat /proc/self/environ and leak GITHUB_APP_PRIVATE_KEY, DATABASE_URL, DAEMON_AUTH_TOKEN, etc.

The allowlist (in src/core/executor.ts buildProviderEnv()):

  • Allowed exact keys: HOME, PATH, USER, LANG, LC_ALL, TZ, TMPDIR, NODE_OPTIONS, NODE_PATH, NODE_NO_WARNINGS, NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, SSL_CERT_DIR, HTTP_PROXY/HTTPS_PROXY/NO_PROXY (uppercase + lowercase), NO_COLOR, FORCE_COLOR, TERM, COLORTERM, CI, GH_TOKEN, GITHUB_TOKEN.
  • Allowed prefixes (forward-compatible for vendor knobs): CLAUDE_CODE_*, ANTHROPIC_*, AWS_*, GIT_*, GH_*.
  • Denied exact keys (override allow): GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_WEBHOOK_SECRET, GITHUB_PERSONAL_ACCESS_TOKEN, DAEMON_AUTH_TOKEN, DAEMON_AUTH_TOKEN_PREVIOUS, DATABASE_URL, VALKEY_URL, REDIS_URL, CONTEXT7_API_KEY.
  • Denied prefixes: GITHUB_APP_*, GITHUB_WEBHOOK_*.

If you add a new env var the agent CLI needs, extend the allowlist in buildProviderEnv(). Anything outside the allowlist is silently dropped — verify by running bun test test/core/build-provider-env.test.ts after the change.

K8s Secret split (defense layer 1b, issue #102)

The Helm chart MUST split secrets into two K8s Secret objects so the daemon Pod's filesystem/environment never carries orchestrator-only credentials, even if the env allowlist above develops a future bug:

Secret object Mounted on Contents
orchestrator-secrets Orchestrator Pod ONLY GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_WEBHOOK_SECRET, DATABASE_URL, VALKEY_URL, CONTEXT7_API_KEY, DAEMON_AUTH_TOKEN[_PREVIOUS] (issuance side).
daemon-secrets Daemon Pod (incl. ephemeral) ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, AWS_* chain (Bedrock provider), DAEMON_AUTH_TOKEN[_PREVIOUS] (handshake side), GITHUB_PERSONAL_ACCESS_TOKEN (PAT mode only).

The orchestrator mints short-lived GitHub installation tokens and forwards them via the WebSocket — daemons never see the App private key or webhook secret.

A startup warning fires if a daemon process detects orchestrator-only env vars at boot — it does NOT crash (a downed daemon is worse than a degraded posture), but the warning surfaces the misconfiguration in operator logs.

Output secret-stripping behavior (defense layer 2)

Every body posted to GitHub is scanned by redactSecrets() — see src/utils/sanitize.ts for the patterns. Detections are SILENTLY STRIPPED (no marker, no footer, no count surfaced in the body) so attackers get no probing feedback. Operator-side info is logged via Pino warn with event: "secret_redacted" carrying kinds, matchCount, callsite, deliveryId — but never the matched bytes.

If redaction empties the body entirely, the GitHub call is skipped and event: "secret_redaction_emptied_body" is logged at error.