Constructdocs
section · guides

Config

15 min read·guides / reference / config

Configuration Reference

Construct follows a strict Secrets vs. State boundary for configuration, aligned with the 12-Factor App principles.

User-scope layout (XDG)

User-scope files follow the XDG Base Directory specification, split across three roots so config, state, and cache can be backed up and pruned independently. Each root honors its $XDG_*_HOME env var when the value is an absolute path, otherwise the default applies (a relative or empty value is ignored). Resolved by lib/config/xdg.mjs.

RootEnv varDefaultHolds
config$XDG_CONFIG_HOME~/.config/constructconfig.env, providers.json, embed.yaml, features.json, claude-ai-mcps.json, custom-credentials.json, provider-subscriptions.json, auth/, boundary.json, config.json, plugins.json, the lib hook symlink
state$XDG_STATE_HOME~/.local/state/constructvector/lancedb, doctor.json, dashboard.json, workspace/, runtime/, bin/, intake-daemon.heartbeat, .cleanup-stamp
cache$XDG_CACHE_HOME~/.cache/constructcache/embeddings, .runtime, regenerable transients

Upgrade — clean break. There is no read or migration of a legacy ~/.construct/* tree. After upgrading, run construct install --scope=user once to repopulate the user-scope files at the XDG paths. construct doctor flags a missing user config until you do.

Config tiers and precedence

Project configuration has two file tiers, most-specific wins:

TierFileCommitted?Merge rule
project-localconstruct.config.local.jsonNo (gitignored)merges over the committed config
projectconstruct.config.jsonYesbase for the local overlay

When both exist, list-shaped settings (e.g. sources.targets, intakePolicy.additionalDirs, permission allowlists) merge and dedupe — object entries collapse on their id so a local re-declaration replaces the committed entry rather than duplicating it; scalars and objects override key-by-key. The local overlay is validated against the same schema (partial allowed — no version required).

Full resolution order for a single setting (most-specific wins): env var (if set) ▷ project-local config ▷ project config ▷ default.

Both config files are JSONC — authored like tsconfig.json. // line comments, /* … */ block comments, and a trailing comma are all tolerated (parsed by lib/jsonc.mjs), so you can keep piped-option hints inline. construct init scaffolds construct.config.json with commented stubs for the switchable settings (deployment.mode: solo | team | enterprise; deployment.mcpBroker: auto | on | off). Comments are stripped on load, so a commented stub never becomes a live value; an uncommented value that fails schema validation (e.g. an out-of-enum deployment.mode) surfaces a clear error and the loader falls back to defaults.

.env precedence and shadow warnings

Environment values are loaded by lib/env-config.mjs from three sources, most-specific wins: project .env ▷ user config.env (XDG config dir) ▷ shell exports. A repo's local .env is closer to the work than a machine-wide default, so it takes the file-tier precedence. (Materialized op run credentials injected into process.env still win over stored op:// references so one 1Password unlock per invocation is enough.)

When a key is defined in both the project .env and the user config.env with different values, Construct prints a one-time shadow warning to stderr naming the key and the winning file, so a stale per-machine default is never silently authoritative. Remove the key from one file to silence it.

The Boundary: Env vs. Config

StoragePurposeShared?Examples
.envSecrets, API keys, and machine-specific local overrides.No (Gitignored)ANTHROPIC_API_KEY, PORT, DATABASE_URL
construct.config.jsonShared, versioned project state and orchestration strategy.Yes (Committed)deployment.mode, telemetry.enabled, orchestration.chainOfThought

Secret Interpolation

To keep secrets out of the committed configuration file while still maintaining a central control plane, construct.config.json supports Environment Variable Interpolation. Any string value starting with $ is replaced with the corresponding environment variable at runtime.

{
  "providers": {
    "anthropic": {
      "apiKey": "$ANTHROPIC_API_KEY"
    }
  }
}

Discoverability & Schema

The construct.config.json file is scaffolded with a "$schema" property. In supported editors (like VS Code or IntelliJ), this provides:

  • Auto-completion: Suggestions for all valid configuration keys.
  • Documentation: Hover over any key to see its purpose and available options.
  • Validation: Immediate feedback if a value is invalid (e.g., a typo in deployment.mode).

Core

VariableDefaultDescription
PORT4242Dashboard HTTP port
BIND_HOST127.0.0.1Dashboard bind address
NODE_ENVdevelopmentproduction disables stack traces in responses
HOMEsystemBase for the XDG user dirs (~/.config/construct, ~/.local/state/construct, ~/.cache/construct) and ~/.cx/
CX_DATA_DIR$HOMEOverride root for .cx/ data directories

Deployment mode

VariableDefaultDescription
CONSTRUCT_DEPLOYMENT_MODEsolosolo | team | enterprise: selects backends for the intake queue, memory, workers, and MCP broker. Read at runtime by lib/deployment-mode.mjs. Set via construct config mode \<m>.

Orchestration

Keys under orchestration in construct.config.json. Read at runtime by lib/orchestration/runtime.mjs.

KeyDefaultDescription
orchestration.workerBackendinline (CLI) / host (MCP, when unset)inline (plan and prepare specialist tasks, no model call) | provider (execute each task against a configured provider model — real API spend) | host (materialize each task's prompt for the calling MCP host to execute in its own model session — no API spend; the host submits results via orchestration_task_result). An MCP-originated orchestration_run call defaults to host when neither the tool's worker_backend arg nor this key is set; the CLI's own default stays inline regardless. host over the bare CLI (no attached MCP session) fails loud rather than producing an abandoned run — see ADR-0063.
orchestration.storefilesystemfilesystem | sqlite | postgres, or a registered kind:"storage" extension manifest whose operations.runStore maps to one of those implementations. Explicit missing backends degrade visibly to filesystem with requestedBackend/degradedReason; they are not silently ignored.
orchestration.chainOfThoughthiddenDisclosure of a provider-executed specialist's reasoning. hidden: reasoning is not requested or shown. surface: reasoning is requested (Anthropic extended thinking / OpenRouter reasoning) and attached to each task, so construct orchestrate run/status, the orchestration_run MCP tool, and the dashboard event stream display it. telemetry_only: reasoning is requested and written to the run trace (.construct/traces/*.jsonl worker.completed metadata) but never displayed. Inline runs never produce reasoning. See ADR-0030.
orchestration.hostExecutionautoOnly meaningful when workerBackend resolves to host. auto: construct-mcp drives the awaiting-host loop itself via MCP sampling (sampling/createMessage) when the connected client declared the sampling capability at initialize time, otherwise the calling host executes each prompt manually and submits it back (pickup). pickup: always leave every task for manual pickup, even if the client supports sampling. sampling: prefer sampling, falling back to pickup if the client never declared the capability. See ADR-0063.

Models (catalog visibility)

Keys under models in construct.config.json. Consumed by lib/models/catalog.mjs and getProviderModelCatalog() in lib/model-router.mjs. Tier assignments (reasoning/standard/fast primaries) remain in specialists/org and emergency overrides in CX_MODEL_* env vars — highest precedence unchanged.

KeyDefaultDescription
models.visibility.modetier_defaultstier_defaults — registry tier primaries + fallbacks only; all_configured — tier defaults plus a capped free OpenRouter slice; explicitmodels.visibility.include allowlist only.
models.visibility.include[]Model ids shown when mode is explicit.
models.visibility.exclude[]Hidden from pickers; pinned model outside visibility shows a warning.
models.visibility.providers{}Per provider-family toggles (openrouter, github-copilot, …); false hides the family.
models.catalog.liveOpenRoutertrueMerge cached live OpenRouter free models into the catalog (~/.cx/model-catalog-cache.json, 10 min TTL).
models.catalog.maxLiveFree24Cap on live free models merged from cache.

CLI:

construct config set models.visibility.mode explicit
construct config set models.visibility.include '["anthropic/claude-sonnet-4-6"]'
construct models list
construct models list --json

Tier-default registry (specialists/org/models.json)

Operators may bind tier defaults in an optional specialists/org/models.json under the toolkit dir (CX_TOOLKIT_DIR, else the installed package root). Each of reasoning/standard/fast is a bare model-id string or { "primary": id, "fallback": [ids] } (resolution uses the primary). The embedded resolver (resolveEmbeddedModel) reads it automatically — no config flag.

Resolution precedence for a tier, most-specific first:

  1. CX_MODEL_\<TIER> / CONSTRUCT_MODEL_\<TIER> env pin — always wins.
  2. specialists/org/models.json registry tier — fills only tiers no env pin sets (resolutionSource: tier-default, tierSource: registry).
  3. Credential-derived provider-family default — see AP3 credential fallback.
  4. config-error — no implicit defaults (ADR-0027): an unconfigured machine degrades honestly rather than resolving a model whose credentials may be absent.

Construct ships no active models.json; copy the shipped specialists/org/models.json.example to activate it.

Deprecated: CONSTRUCT_MODEL_* env vars — use CX_MODEL_* (alias still honored for one release cycle).

Typed integration selectors under sources.targets. Consumed by embed auto-discovery (when embed.yaml is absent), provider_fetch, and session-start source hints. Legacy env lists merge additively; an explicit embed.yaml in the XDG config dir remains a complete override.

{
  "sources": {
    "targets": [
      { "id": "gh-main", "provider": "github", "selector": { "repo": "org/repo" } },
      { "id": "jira-plat", "provider": "jira", "selector": { "project": "PLAT" } },
      { "id": "linear-eng", "provider": "linear", "selector": { "team": "ENG" } },
      { "id": "slack-incidents", "provider": "slack", "selector": { "channel": "incidents", "intent": "risk" } }
    ]
  }
}

CLI: construct sources list, construct sources add \<provider> \<id> '\<selector-json>', construct sources remove \<id>, construct sources validate.

Env (legacy merge)Maps to
GITHUB_REPOSGitHub selector.repo entries
JIRA_PROJECTSJira selector.project entries
LINEAR_TEAMSLinear selector.team entries
SLACK_CHANNELSSlack selector.channel (+ optional :intent)

Intake policy (construct.config.json)

Filesystem inbox watcher depth and extra directories under intakePolicy. A legacy .cx/intake-config.json is read as a warned fallback for maxDepth and additionalDirs only.

The single canonical drop zone is inbox/ at the project root (ADR-0045 §C) — always watched. There are no other zones: .construct/inbox/ and docs/intake/ are not watched and not scaffolded. Machine/runtime intake state (pending, processed, skipped, quarantine, dead-letter) stays under the gitignored .construct/intake/.

KeyDefaultDescription
intakePolicy.maxDepth4Subdirectory scan depth per watched directory
intakePolicy.additionalDirs[]Extra directories to watch beyond inbox/ (opt-in only)

Drop convention (atomic handoff). Writers assemble a file under inbox/.staging/ (gitignored) and then atomically rename it into inbox/. The watcher enqueues only complete top-level files: it ignores dotfiles and inbox/.staging/, and skips any file whose size is still changing between two stats, so a partially-written drop is never consumed. Processed items move to .construct/intake/processed/.

Env overrides: CX_INBOX_DIRS (colon-separated paths), CX_INTAKE_MAX_DEPTH.

Intake queue

VariableDefaultDescription
CONSTRUCT_INTAKE_QUEUE_BACKENDmode defaultfilesystem | git | postgres override. Wins over CONSTRUCT_DEPLOYMENT_MODE. Mode defaults are filesystem for solo and postgres for team/enterprise. postgres requires DATABASE_URL/CONSTRUCT_DATABASE_URL; team/enterprise only fall back to git when CONSTRUCT_DEGRADED_OK=postgres-queue is explicitly set.
CONSTRUCT_PROJECT_NAMEbasename of CWDProject scope for Postgres-backed intake queue rows.
CONSTRUCT_INTAKE_QUEUE_NAMEintakeLogical queue name for the Postgres queue provider.
CONSTRUCT_QUEUE_LEASE_SECONDS120Claim lease duration for the Postgres queue provider before an unhearted claim is reclaimable.
CONSTRUCT_TENANT_IDlocal outside enterpriseTenant scope for enterprise mode and Postgres queue rows.
CONSTRUCT_DEBUG_INTAKEunset1 to log daemon-side intake preparation failures to stderr (non-fatal otherwise).

Worker runtime

VariableDefaultDescription
CONSTRUCT_WORKER_ID\<hostname>:\<pid>Stable worker identity stored in the Postgres worker registry.
CONSTRUCT_WORKER_TTL_SECONDS120Worker heartbeat TTL before construct workers list reports the worker as stale.

MCP broker

Broker engagement resolves with the same env ▷ project config ▷ default precedence as deployment mode (isBrokered() in lib/mcp/broker.mjs).

KeyDefaultDescription
CONSTRUCT_MCP_BROKER (env)(unset)on | off: overrides broker engagement regardless of config or deployment mode.
deployment.mcpBroker (construct.config.json)autoon | off | auto: auto defers to deployment mode (broker on in team/enterprise, off in solo).

Authentication

VariableDescription
CONSTRUCT_DASHBOARD_TOKENBearer token for single-token dashboard auth (simple path)
CONSTRUCT_DASHBOARD_ORIGINSComma-separated CORS origin allowlist (e.g. http://localhost:4242)
CONSTRUCT_AUTO_INSTALL1 to auto-install optional resources without prompting (CI use)

Embedding

VariableDefaultDescription
CONSTRUCT_EMBEDDING_MODELhashinghashing (offline), local-onnx (384d), openai (1536d)
CONSTRUCT_EMBEDDING_FALLBACK:1 to silently fall back to local-onnx when openai key missing
OPENAI_API_KEY:Required when CONSTRUCT_EMBEDDING_MODEL=openai

Database

VariableDescription
DATABASE_URLPostgreSQL connection string (e.g. postgresql://user:pass@localhost:5432/construct). When set with the optional postgres package installed, construct db status probes the connection and Postgres run-store/queue selection can use it.
CONSTRUCT_DATABASE_URLAlias for DATABASE_URL used by Construct-specific launch environments.
CONSTRUCT_DB_MAX_CONNECTIONSOptional Postgres.js pool size override for Construct SQL clients; default 5.
CONSTRUCT_DB_IDLE_TIMEOUT_SECONDSOptional Postgres.js idle timeout; default 20.
CONSTRUCT_DB_CONNECT_TIMEOUT_SECONDSOptional Postgres.js connect timeout; default 10.
CONSTRUCT_DB_SSLOptional Postgres.js SSL mode (false, true, require, prefer, verify-full).

Run construct db migrate to apply Construct-owned Postgres migrations idempotently; construct db status --json reports connection and migration state without printing credentials. Queue tables are namespaced by project, tenant_id, and queue_name; claims use FOR UPDATE SKIP LOCKED plus lease expiry so a crashed worker's item becomes reclaimable.

Providers

VariableDescription
GITHUB_TOKENGitHub personal access token. Alias: GH_TOKEN. Without this, unauthenticated rate limits apply (60 req/h).
GH_TOKENAlias for GITHUB_TOKEN: checked when GITHUB_TOKEN is absent

1Password (op://) credentials

Store provider keys in config.env (the XDG config dir, default ~/.config/construct/config.env) as op://vault/item/field references (or plain values). Construct resolves them lazily via op read when a model call needs the plaintext.

Recommended (one auth for the whole dev tree): point Construct at a shared op run env file and let it resolve once. Set CONSTRUCT_OP_ENV_FILE in config.env:

# ~/.config/construct/config.env
CONSTRUCT_OP_ENV_FILE=~/.config/claude/.env.op

With that set and op installed, construct dev re-execs itself once under a single op run --env-file …, so every op:// reference resolves one time (one biometric unlock) and every detached daemon inherits the resolved keys. You do not need to wrap the CLI by hand.

op run masks secrets in the wrapped process output by default. Because the daemons are detached and write their own log files, that masking covers the construct dev process output but not the daemon logs (local-only, under the state dir) — see ADR-0049. For a standalone tool you can still wrap manually — op run --env-file="$HOME/.config/claude/.env.op" -- opencode — with masking on.

When op run injects materialized keys into process.env, Construct keeps those values instead of overwriting them with stored op:// refs from config.env. Override the env-file location with CONSTRUCT_OP_ENV_FILE and the wrapped binary with CONSTRUCT_BIN.

Bare-node (MCP server) context. The MCP server is launched as bare node lib/mcp/server.mjs — no op run wrapper — so an op:// reference resolves via a direct spawnSync('op', ['read', opRef]) inside lib/providers/secret-resolver.mjs instead of the pre-resolved process.env path above. This requires the op CLI to be reachable on the launching process's PATH and authenticated through one of two mechanisms: 1Password desktop-app CLI integration (Settings → Developer → Integrate with 1Password CLI, then unlock the app) or OP_SERVICE_ACCOUNT_TOKEN for non-interactive/headless contexts (a bare op whoami can still report "not signed in" under desktop-app integration — it checks account-level session state, while op read authenticates per call through the desktop app's IPC channel). Without either, op read throws OP_NOT_SIGNED_IN. | GITHUB_REPOS | Comma-separated owner/repo list surfaced as provider source hints at session start | | JIRA_BASE_URL | Atlassian Jira base URL (e.g. https://yourorg.atlassian.net) | | JIRA_EMAIL | Jira account email: used for Basic auth | | JIRA_API_TOKEN | Jira API token (from id.atlassian.com/manage-profile/security/api-tokens) | | CONFLUENCE_BASE_URL | Confluence base URL: defaults to JIRA_BASE_URL if unset | | CONFLUENCE_EMAIL | Confluence account email: defaults to JIRA_EMAIL if unset | | CONFLUENCE_API_TOKEN | Confluence API token: defaults to JIRA_API_TOKEN if unset | | SLACK_BOT_TOKEN | Slack bot OAuth token (xoxb-...) | | SLACK_USER_TOKEN | Slack user token (xoxp-...): required for search.messages | | SALESFORCE_INSTANCE_URL | Salesforce instance URL (e.g. https://yourorg.my.salesforce.com) | | SALESFORCE_ACCESS_TOKEN | Salesforce OAuth access token (bearer) |

Telemetry & Tracing

VariableDefaultDescription
CONSTRUCT_TRACE_BACKENDlocalTrace backend: local, langfuse, http, otel, or none. Legacy remote resolves to langfuse when keys are present, otherwise http when a URL is set.
CONSTRUCT_TELEMETRY_URL:Langfuse-compatible or generic HTTP ingestion endpoint
CONSTRUCT_TELEMETRY_PUBLIC_KEY:Langfuse-compatible public key
CONSTRUCT_TELEMETRY_SECRET_KEY:Langfuse-compatible secret key
CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT:OTLP HTTP collector endpoint used when CONSTRUCT_TRACE_BACKEND=otel
CONSTRUCT_TELEMETRY_PROVIDERderivedOptional display label for dashboard/status

MCP

VariableDefaultDescription
CONSTRUCT_ORCHESTRATION_URL(unset)Point orchestration_run/orchestration_status at a remote/team orchestration service over HTTP. Unset = in-process (solo default — no daemon, no port, no token).
CONSTRUCT_ORCHESTRATION_TOKEN(unset)Bearer token for the remote orchestration service (falls back to CONSTRUCT_DASHBOARD_TOKEN).

Logging

VariableDefaultDescription
CONSTRUCT_LOG_LEVELinfoLog verbosity: debug | info | warn | error
CONSTRUCT_LOG_PRETTY01 for human-readable output instead of JSONL

Memory

VariableDefaultDescription
CONSTRUCT_MEMORYonSet to off to disable memory injection for a session. Stats still recorded.

Session & drop zone

VariableDefaultDescription
CONSTRUCT_DROP_DIRS~/Downloads:~/Desktop:~/DocumentsColon-separated dirs watched by construct drop
CONSTRUCT_HOOK_OUTPUT_MODEautoSessionStart context routing: auto | silent | stderr | stdout. auto keeps the rich payload on stdout for interactive sessions and suppresses it (to ~/.cx/session-start-last.log) for non-interactive ones. Mirrors hooks.outputMode in construct.config.json (env wins). Set to silent/stderr from SDK / claude -p / automation callers so a one-shot command's stdout stays reserved for its own output.
CONSTRUCT_NONINTERACTIVE:Set truthy (1) by an SDK / automation caller to mark the invocation non-interactive, so CONSTRUCT_HOOK_OUTPUT_MODE=auto resolves to suppressed. Claude Code exposes no reliable in-hook interactive/print signal, so this flag (or CI=true / NODE_ENV=test) is how auto detects non-interactive mode.

Model router

VariableDescription
OPENROUTER_API_KEYOpenRouter key: enables free model fallback when primary provider is down
ANTHROPIC_API_KEYAnthropic key for Claude model calls
OPENAI_API_KEYOpenAI key: used for OpenAI model tier or openai embedding model
GITHUB_TOKENGitHub PAT for integrations; may be a plain value or op:// reference
CONSTRUCT_OP_ENV_FILEOverride path for the 1Password env file used by a local op run wrapper (default on this machine: ~/.config/claude/.env.op)
CX_MODEL_REASONINGOverride the reasoning-tier model id
CX_MODEL_STANDARDOverride the standard-tier model id
CX_MODEL_FASTOverride the fast-tier model id
CONSTRUCT_MODEL_PROFILEOptional runtime profile. small enables tighter prompt budgets, compressed overlays, and retrieval-first prompt shaping for smaller local or cost-constrained models; balanced keeps the default posture.
OLLAMA_BASE_URLBase URL for the Ollama HTTP API. OLLAMA_HOST remains accepted as a legacy alias.

Deprecations & Debug

VariableDescription
CONSTRUCT_DEPRECATIONSerror to throw instead of warn on deprecated API usage (useful in CI)
CONSTRUCT_DEV_PATHAbsolute path to a Construct checkout; .construct/launcher/run.mjs resolves this first
CX_AUTO_EMBED1 to auto-start the embed daemon when provider credentials are present. construct.config.json autoEmbed: true is an equivalent project-committed fallback (env wins); read at runtime by autoStartEmbedIfNeeded() in lib/embed/cli.mjs.
CX_WORKSPACEOverride working directory for embed mode
CX_TOOLKIT_DIROverride the path where Construct looks for its own toolkit (skills, agents, templates)

Bootstrap resource consent

Each optional resource stores operator consent in config.env (the XDG config dir) via a BOOTSTRAP_\<RESOURCE> key (values: yes, never, blank = not yet asked).

KeyResource
BOOTSTRAP_POSTGRESPostgreSQL + pgvector (Docker container)
BOOTSTRAP_EMBEDDING_MODELLocal ONNX embedding model (~50 MB)