Every environment variable this repo reads or writes, grouped by the runtime that owns it. The runbook is DEVELOPMENT.md; release mechanics are RELEASE.md.
There is no .env template — devenv.nix sets dotenv.disableHint = true and nothing here requires an env var to boot. Everything below is an override or a CI-only input.
Two rules that are easy to get wrong:
- Build-time variables must be declared to turbo. Only
apps/desktop/turbo.jsonandapps/webview/turbo.jsondeclarebuild.env; the rootturbo.jsonhas noglobalEnv. A new*_VITE_*variable that isn't listed there silently reuses a cached bundle built with the old value. LINKCODE_PORTmeans two different things. For the daemon it overrides the listener port (apps/daemon/src/config.ts); for a user script/service it is written by the script runner to announce that service's allocated port (packages/host/engine/src/scripts/script-port-plan.ts).
Read by the daemon, desktop, webview, or mobile at run time.
| Variable | Read at | Effect |
|---|---|---|
LINKCODE_CHANNEL |
apps/daemon/src/paths.ts |
Picks the daemon's on-disk universe: release → ~/.linkcode + ~/LinkCode + …/LinkCode/assets; development → the LinkCode Development / .linkcode.development set (CODE-460). Outranks the build-time stamp, which is why the desktop supervisor always injects its own CHANNEL — the devshell pack's bundled daemon is stamped release. Any other value aborts boot. |
LINKCODE_BUILD_CHANNEL |
apps/daemon/tsup.config.ts (define) |
Build-time stamp, not a runtime knob. tsup replaces the literal with release, so a built daemon defaults to release and the TS source defaults to development. Setting it by hand in a shell works but is not the supported override — use LINKCODE_CHANNEL. |
LINKCODE_PROFILE |
apps/daemon/src/config.ts |
Isolated state universe within a channel: forks the state dir to the -<name> sibling (~/.linkcode.development-alpha), plus DB, runtime.json, and HQ device identity. [a-z0-9-], ≤32 chars; invalid aborts boot. Workspaces and the asset store do not fork by profile. Desktop reads it too, where --profile=<name> outranks it, and re-injects the resolved value into the supervised daemon. Unset = the channel's default universe. |
LINKCODE_PORT |
apps/daemon/src/config.ts |
Overrides every configured listener's port. Must parse as an integer in 1..65535, otherwise the config value stands. |
LINKCODE_HOST |
apps/daemon/src/config.ts |
Overrides every listener's bind host. |
LINKCODE_PTY_SIDECAR_PATH |
apps/daemon/src/pty/sidecar.ts |
Absolute path to the linkcode-pty binary; always wins. Dev falls back to target/release/linkcode-pty; a bundled dist/ daemon has no fallback and disables terminals. The packaged desktop supervisor sets it to <resourcesPath>/sidecar/<arch>. |
LINKCODE_SIM_SIDECAR_PATH |
apps/daemon/src/sim/backend.ts |
Absolute path to the linkcode-sim iOS Simulator sidecar; always wins. macOS only — other platforms resolve to none regardless. Dev falls back to target/release/linkcode-sim; a bundled dist/ daemon has no fallback and disables simulators. The packaged desktop supervisor sets it from <resourcesPath>. |
LINKCODE_AIGATEWAY_PATH |
apps/daemon/src/ai-gateway.ts |
Path to the aigateway translation sidecar, overriding the managed-asset install. |
LINKCODE_ASSETS_DIR |
packages/host/assets/src/paths.ts |
Redirects the managed-asset store root (default: ~/Library/Application Support/LinkCode/assets, %LOCALAPPDATA%/LinkCode/assets, $XDG_DATA_HOME/linkcode/assets). Resolved per call, so tests can stub it. |
ELECTRON_RENDERER_URL |
apps/desktop/src/main/window.ts |
Dev-server URL the main process loads instead of the packaged renderer. Written by apps/desktop/scripts/dev.mts. |
HTTP_PROXY, HTTPS_PROXY, NO_PROXY (and lowercase forms) |
packages/host/assets/src/system-proxy/index.ts |
If any proxy variable is non-empty, OS proxy autodetection is skipped and the fetch layer's own env handling takes over. NO_PROXY is merged back in when an OS-detected proxy is passed explicitly. |
SHELL, COMSPEC |
apps/daemon/src/pty/sidecar.ts; packages/host/agent-adapter/src/shell-env.ts |
Default PTY shell (/bin/bash / cmd.exe when unset). On macOS, terminals and Codex/Claude agent processes load the user's login shell because a Finder-launched app inherits launchd's bare PATH. |
CODEX_HOME, PI_CODING_AGENT_DIR |
packages/host/agent-adapter/src/native/{codex,pi}/history.ts |
Agent CLI home directories the adapters read history and auth from (~/.codex, ~/.pi/agent). |
Not configuration you set — the daemon produces these for the processes it spawns.
On macOS, Codex and Claude start with the environment returned by the user's login shell in the
project directory. If direnv is available after shell startup, the project environment is applied
too. The resolved environment is cached for the session, account extraEnv is merged last, and
Codex config/history paths use the same environment. Other platforms retain the daemon environment.
| Variable | Written by | Meaning |
|---|---|---|
LINKCODE_PORT, LINKCODE_URL |
packages/host/engine/src/scripts/script-port-plan.ts |
A user service's own allocated port and preview-proxy URL. |
LINKCODE_SERVICE_<NAME>_PORT, LINKCODE_SERVICE_<NAME>_URL |
same | Sibling-service discovery; <NAME> is the service name upper-cased with non-word characters replaced by _. |
ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, CODEX_API_KEY, OPENAI_BASE_URL, XAI_API_KEY |
packages/host/agent-adapter/src/credential.ts |
Account credentials handed to the agent subprocess. With token auth, ANTHROPIC_API_KEY is explicitly blanked so an inherited key can't defeat the bearer token. |
account extraEnv |
packages/foundation/schema/src/model/account.ts |
A user-defined Record<string, string> merged last into every agent subprocess. Open-ended by design — this is the supported escape hatch for agent-specific variables the adapters don't model. |
ELECTRON_RUN_AS_NODE, ELECTRON_NO_ATTACH_CONSOLE, LINKCODE_RESOLVING_ENVIRONMENT |
packages/host/agent-adapter/src/shell-env.ts |
Temporary flags used only while the login shell reports its environment. They make the packaged Electron runtime execute the JSON probe as Node, suppress console attachment, and let shell startup skip interactive-only work. Original values are restored before the agent starts. |
TERM |
crates/linkcode-pty/src/pty.rs |
Hard-set to xterm-256color on every PTY child, then overlaid with the caller's env. The sidecar itself reads no environment. |
Point a client at something other than production LinkCode Cloud. All default to the production endpoints; unset is the normal case.
| Variable | Read at | Default |
|---|---|---|
LINKCODE_CLOUD_API_URL |
apps/desktop/src/main/cloud-auth/client.ts |
https://api.linkcode.ai |
LINKCODE_CLOUD_SIGN_IN_URL |
apps/desktop/src/main/cloud-auth/client.ts |
https://linkcode.ai/sign-in |
VITE_LINKCODE_CLOUD_API_URL |
apps/webview/src/cloud/auth.ts |
https://api.linkcode.ai (build-time inlined) |
LINKCODE_CLOUD_URL |
apps/daemon/src/cloud/login.ts |
DEFAULT_CLOUD_URL in apps/daemon/src/cloud/api.ts |
EXPO_PUBLIC_CLOUD_URL |
apps/mobile/src/runtime/cloud/client.ts |
https://api.linkcode.ai (inlined by Metro/EAS; local svc dev stacks only) |
EXPO_PUBLIC_IDP_URL |
apps/mobile/src/runtime/cloud/idp.ts |
https://auth.arcbox.dev (inlined by Metro/EAS; local svc dev stacks only) |
LINKCODE_CLOUD_URL falls back on an empty string (||); the desktop/webview overrides use ??, so setting them to '' yields an empty base URL rather than the default.
DSNs and PostHog project tokens are publishable ids, not secrets. Unset means the SDK no-ops — the default for local development. Product analytics also requires an explicit user opt-in in each client.
These changes do not client-sample Sentry error events. Performance root traces use client-side sampling: 10% in desktop, webview, and mobile, and 5% in the daemon. On each daemon start, optional CPU profiling is enabled with a 1% probability; in selected runs it follows sampled trace activity. Sentry project-level dynamic sampling can reduce retained trace volume remotely, but it does not prevent the client from uploading a sampled trace, so bandwidth-sensitive changes still require a client configuration or new build.
| Variable | Surface | Notes |
|---|---|---|
LINKCODE_SENTRY_DSN |
Daemon process | Read by apps/daemon/src/instrument.ts, preloaded via --import. The desktop supervisor copies MAIN_VITE_SENTRY_DSN into it for the packaged daemon. |
MAIN_VITE_CONFIG_BOOTSTRAP |
Electron main | Build-time immutable JSON containing the config target, HTTPS endpoints, public verification keyrings, schema version, and bundled defaults. Unset builds stay READY on empty bundled defaults with remote refresh disabled. Local override files are read only by unpackaged builds. Setting it while apps/desktop/generated/config-build-bundle.json exists is a hard build error — a rendered bundle cannot be overridden. |
LINKCODE_REQUIRE_CONFIG_BUNDLE |
Desktop build/packaging | 1 makes the Vite main build fail without a rendered apps/desktop/generated/config-build-bundle.json and makes verify-artifacts.mts require the staged asar copy. Set by build-desktop.yml on signed builds (RELEASE.md, "Immutable config bundle"). |
MAIN_VITE_SENTRY_DSN |
Electron main | Build-time inlined; declared in apps/desktop/turbo.json build.env. Only signed builds carry it. |
VITE_SENTRY_DSN |
Webview | Build-time inlined; declared in apps/webview/turbo.json build.env. |
EXPO_PUBLIC_SENTRY_DSN |
Mobile | Inlined by Metro/EAS at bundle time. |
SENTRY_ALLOW_FAILURE |
EAS preview profile |
apps/mobile/eas.json; keeps a failed sourcemap upload from failing the build. |
RENDERER_VITE_POSTHOG_PROJECT_TOKEN, RENDERER_VITE_POSTHOG_HOST |
Desktop renderer | Build-time inlined; both are required and declared in apps/desktop/turbo.json. Accepts only an HTTPS PostHog Cloud host (*.posthog.com) because Electron CSP is locked to that origin family. Only explicit app, connection, thread-lifecycle, turn-submission, and identity events are allowed; autocapture and replay are disabled. |
VITE_POSTHOG_PROJECT_TOKEN, VITE_POSTHOG_HOST |
Webview | Build-time inlined; both are required and declared in apps/webview/turbo.json. Uses the same explicit-event policy as desktop. |
EXPO_PUBLIC_POSTHOG_PROJECT_TOKEN, EXPO_PUBLIC_POSTHOG_HOST |
Mobile | Inlined by Metro/EAS. Both are required; local development remains disabled even when set. Autocapture, lifecycle capture, replay, error tracking, and remote flags are disabled. |
| Variable | Used by | Effect |
|---|---|---|
LINKCODE_REQUIRE_PTY_SIDECAR |
apps/daemon/tests/integration/pty-sidecar.test.ts, terminal-flood.test.ts |
1 turns a missing linkcode-pty binary from a silent skip into a hard failure. CI sets it; set it locally too when you mean to exercise the real wire protocol. |
LINKCODE_PTY_SIDECAR_PATH |
apps/daemon/e2e/startup.e2e.ts |
Points the spawned daemon at the compiled sidecar (CI uses target/debug/linkcode-pty). |
LINKCODE_HOST, LINKCODE_PORT |
daemon/webview/desktop E2E harnesses | Pin the harness daemon to 127.0.0.1 on an ephemeral port. |
LINKCODE_PROFILE |
desktop E2E | Isolates a run's state universe. Must be identical on both sides — the desktop app and its daemon — or they follow different runtime.json files. The same applies to LINKCODE_CHANNEL when a harness sets it. |
HOME |
every E2E harness | Redirected to a fresh temp dir so runs never touch the real state dirs. Use a fresh one per run. |
LINKCODE_E2E_KEEP_OPEN |
apps/desktop/e2e/simulator-panel.e2e.mts |
1 hands the app over at the pause and waits for you to close the window instead of running a timer. The two section-close checks after the pause are given up in exchange — the alternative is yanking the window away from whoever is driving it. |
LINKCODE_E2E_HOLD_MS |
apps/desktop/e2e/simulator-panel.e2e.mts |
Widens the pause that leaves the window live to be driven by hand (default 30000). For demoing the simulator panel rather than checking it. |
LINKCODE_E2E_SKIP_RECLAIM |
apps/desktop/e2e/simulator-panel.e2e.mts |
1 drops the closing CODE-419 reclaim check, whose last act is to SIGTERM the daemon — correct in a test, looks like a crash in a demo. The run then proves everything except reclaim-on-shutdown. |
XDG_CONFIG_HOME |
apps/desktop/e2e/packaged-smoke.e2e.mts |
Redirects Electron/Chromium config for the packaged run. |
NODE_ENV |
apps/webview/e2e/browser-smoke.e2e.mts |
Forced to development — the mock transport is guarded by import.meta.env.DEV. |
GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM |
packages/host/engine/tests/integration/git-status.test.ts |
Point git at fixture config so the machine's gitconfig (notably commit signing) can't leak into assertions. |
LINKCODE_PTY_BENCH_BYTES, _RUNS, _WARMUP_RUNS |
apps/daemon/src/pty/bench-throughput.mts |
Throughput benchmark payload size (default 8 MiB), measured runs (5), warm-up runs (1). |
| Variable | Used by | Effect |
|---|---|---|
NODE_ENV |
apps/desktop/scripts/{build,dev}.mts |
Defaulted before Vite runs because it seeds import.meta.env.MODE, which decides the desktop channel (development vs release) and therefore app name, userData, instance lock, and keychain entry. |
RENDERER_VITE_*, VITE_* |
apps/desktop/vite.renderer.config.ts |
The only prefixes exposed to desktop renderer code (envDir is apps/desktop). |
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER |
apps/desktop/scripts/stage-sidecar.mts |
aarch64-linux-gnu-gcc for the linux-arm64 sidecar cross-build. |
NODE_OPTIONS |
.github/workflows/ci.yml |
--max-old-space-size=4096 for every CI job. |
POSTHOG_HOST |
desktop/mobile build workflows | Organization Actions variable mapped to the platform-specific PostHog host for production bundles. |
CONFIG_PUBLISHER_REPO, CONFIG_SOURCE_REPO |
release workflows | Protected release Environment vars. They are different canonical arcboxlabs/repository identities for publisher code and structural source data; trusted steps validate them before minting separate repository-scoped read tokens. Current values are arcboxlabs/linkcodehq and arcboxlabs/linkcode-config. Official releases download digest-bound revision/keyring/manifest bytes from config.linkcode.ai instead of Actions variables. The nonproduction brand-matrix fixture retains compatibility inputs documented in RELEASE.md. |
Set as GitHub repository/environment secrets, never locally. Signing and notarization secrets live in the protected release environment; unsigned PR builds get empty values and skip signing. The release automation GitHub App credentials are repository/org secrets because release-please must maintain PRs before entering that environment. Full context in RELEASE.md.
| Variable | Where | Purpose |
|---|---|---|
MACOS_CSC_LINK, MACOS_CSC_KEY_PASSWORD |
build-desktop.yml |
Developer ID certificate and password. Deliberately not named CSC_*: electron-builder treats a set-but-empty CSC_LINK as a certificate path and dies. The run script re-exports them as CSC_LINK/CSC_KEY_PASSWORD only when non-empty. |
CSC_IDENTITY_AUTO_DISCOVERY |
build-desktop.yml |
false on unsigned builds so macOS can't sign with a random keychain identity. |
APPLE_API_KEY_BASE64 → APPLE_API_KEY |
build-desktop.yml |
The App Store Connect .p8 is materialized to $RUNNER_TEMP/apple_api_key.p8; electron-builder wants a file path, not the key content. |
APPLE_API_KEY_ID, APPLE_API_ISSUER, APPLE_TEAM_ID |
build-desktop.yml |
notarytool key identity and team. |
EXPO_TOKEN |
build-mobile.yml |
Expo robot-user token with access to the LinkCode EAS project, managed build credentials, remote build versions, and EAS Submit. Store it in release only after enabling required reviewers and deployment branch/tag restrictions. |
SENTRY_AUTH_TOKEN |
build-mobile.yml |
Organization Actions secret that uploads production mobile source maps. Local EAS Build cannot read an EAS variable with Secret visibility, so GitHub must inject it. |
SENTRY_DSN_DESKTOP, SENTRY_DSN_MOBILE, POSTHOG_PROJECT_TOKEN |
desktop/mobile build workflows | Mapped to platform build-time telemetry variables. These are publishable identifiers, but the repository currently carries them as Actions secrets. |
AZURE_PUBLISHER_NAME, AZURE_SIGN_ENDPOINT, AZURE_CODE_SIGNING_ACCOUNT, AZURE_CERTIFICATE_PROFILE |
build-desktop.yml |
Windows Trusted Signing identifiers (not credentials, but kept as secrets so the public repo doesn't advertise the signing infrastructure). AZURE_PUBLISHER_NAME must match the certificate subject CN exactly. |
AZURE_TENANT_ID, AZURE_CLIENT_ID |
build-desktop.yml |
azure/login inputs for OIDC federation. No AZURE_* credential env exists during packaging on purpose, so DefaultAzureCredential falls through to the Azure CLI entry. |
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY |
release-desktop.yml |
Cloudflare R2 credentials for publishing the electron-updater feed. AWS_REQUEST_CHECKSUM_CALCULATION/AWS_RESPONSE_CHECKSUM_VALIDATION are pinned to WHEN_REQUIRED because R2 doesn't implement the checksums recent aws-cli sends. |
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY |
release-brand-matrix.yml |
R2 account and S3 credentials in the protected release Environment. Scope the key pair to object read/write/list only for the exact bucket/prefix destinations in the reviewed matrix. |
BOT_APP_ID, BOT_APP_PRIVATE_KEY |
release and config-render workflows | Organization GitHub App credentials. The App needs Contents, Issues, and Pull requests read/write on this repo so release-please can maintain PRs, draft Releases, and tags; install it on the private repositories selected by CONFIG_PUBLISHER_REPO and CONFIG_SOURCE_REPO so config rendering can mint separate short-lived tokens restricted to Contents read on each repository. Package-manager bumps additionally require installations on arcboxlabs/homebrew-tap and arcboxlabs/winget-pkgs with contents + pull-requests write. Missing credentials fail release automation before any tag is created; only package-manager bumps remain an optional self-skip. |
Mobile certificates, provisioning profiles, the Android keystore, the App Store Connect API key, and the Google Play service-account key are EAS-managed credentials, not GitHub variables.