Configuration
Configuration reference
The annotated example configuration is maintained in config.example.toml.
# Every command uses the mandatory platform sandbox: Bubblewrap on Linux,
# Seatbelt (sandbox-exec) on macOS, or the embedded AppContainer on Windows.
# Latch refuses to execute commands without its platform boundary.
default_mode = "WORK"
# Storage root defaults to ~/.latch/ (config.toml, secrets.toml, latch.sqlite3,
# artifacts/, cache/) and is created 0700. An explicit state_dir overrides it.
# Existing ~/.config/latch + ~/.local/state/latch installs keep working in
# place until `latch migrate` copies them explicitly.
# state_dir = "/home/you/.latch"
# Recommended multi-provider configuration. Each provider is a named instance
# with a symbolic credential reference; secret values never live in this file.
#
# ChatGPT subscription: run `latch login` or `/login` (device authorization).
# This is separate from OpenAI API billing. Plan eligibility/limits apply.
# [providers.openai-codex]
# kind = "openai-codex"
# credential = "oauth:openai-codex" # managed tokens; never paste tokens here
# default_model = "gpt-6.1-sol"
# The Codex endpoint is fixed; arbitrary base_url overrides are refused.
#
# [providers.opencode-go]
# kind = "opencode-go" # openai | openai-codex | anthropic | deepseek | opencode-go | opencode-zen | openai-compatible
# credential = "env:OPENCODE_API_KEY" # env:NAME | file:NAME (0600 local store).
# keyring:NAME parses for forward compatibility but is not available in this
# build and is never advertised by /setup.
# default_model = "deepseek-v4-flash"
# One key enables the whole catalog; omit enabled_models for all models.
# /model refresh updates service metadata; /model all clears local filters.
# enabled_models = ["deepseek-v4-flash"] # optional intentional subset
# Discovered [providers.<id>.catalog] metadata is managed by Latch; user
# [providers.<id>.models] overrides remain separate and win field by field.
# base_url = "https://opencode.ai/zen/go/v1" # optional override
#
# [providers.opencode-go.models."deepseek-v4-flash"]
# display_name = "DeepSeek V4 Flash"
# context_window_tokens = 1048576
# efforts = ["none", "low", "high", "max"]
# default_effort = "high"
# reasoning_replay = "replay"
# transport = "chat_completions" # chat_completions | responses | anthropic_messages | gemini
# adaptive_thinking = false # Anthropic adaptive thinking
# input_modalities = ["text", "image"] # explicit image input for a custom model
# aliases = ["flash", "deepseek-v4.1-flash"]
# [providers.opencode-go.models."deepseek-v4-flash".pricing]
# input_per_million = 0.28
# output_per_million = 0.42
#
# Custom/Advanced per-level wire mapping. A present map must cover every
# exposed level; forms a transport cannot express are rejected at validation.
# Built-in models use their adapter mapping and need no copy here.
# [providers.custom.models."acme-pro".effort_map]
# low = { value = "1" }
# medium = { value = "2" }
# high = { budget_tokens = 32768 }
# none = { disabled = true }
#
# Gemini thinking is model-specific, never transport-wide: the same transport
# carries level models, budget models, models where zero disables thinking,
# and models that cannot disable thinking. Built-in Gemini rows carry their
# documented capability automatically; a custom Gemini model declares it
# before exposing efforts or mapping them. `mode = "levels"` lists the
# documented thinkingLevel values and `off` names the no-thinking level when
# the model has one; `mode = "budget"` uses thinkingBudget and `zero_allowed`
# is the documented off switch.
# [providers.custom.models."gemini-3.8-flash".gemini_thinking]
# mode = "levels"
# levels = ["low", "medium", "high"]
#
# [providers.custom.models."gemini-2.5-flash".gemini_thinking]
# mode = "budget"
# zero_allowed = true
# Default inference profile. Precedence at startup:
# CLI override > durable session profile (resume) > this config > built-in.
# [inference]
# provider = "opencode-go"
# model = "deepseek-v4-flash"
# effort = "low" # per-model: provider default | none | minimal | low | medium | high | xhigh | max
# Backward compatibility: the legacy single-provider table below is migrated
# into the registry automatically when no [providers.*] entries exist. New
# configurations should prefer the multi-provider shape above.
[provider]
kind = "openai-compatible" # or "anthropic"
model = "gpt-5-mini"
# base_url = "https://api.openai.com/v1"
# api_key_env = "OPENAI_API_KEY"
# OpenCode Go (base_url under https://opencode.ai/zen/go/v1) uses the same
# provider and additionally receives a stable x-opencode-session header.
# Optional operator-installed language server; starts lazily, read-only/offline.
# Omit this section for no extra tool schema/process. See docs/LSP.md.
# [[lsp_servers]]
# name = "clangd"
# command = "clangd"
# args = ["--background-index=false", "--clang-tidy=false", "--enable-config=false"]
# extensions = ["cpp", "hpp"]
# language_id = "cpp"
# timeout_seconds = 30
# Model network deadlines (seconds), separate from shell/validation timeouts.
# New-connection/TLS time is included in the header budget, not added to it.
# Heartbeats/reasoning reset idle only; 180s is not a thinking-duration limit.
# Each value must be 1..86400. See docs/PROVIDER_REQUESTS.md for retry semantics.
[provider_request]
connect_timeout_seconds = 15
headers_timeout_seconds = 60
idle_timeout_seconds = 180
request_timeout_seconds = 1800
[safety]
# Strict | Standard | Autonomous. Safety decides Allow/Ask/Deny; it never
# changes what the Mode permits.
level = "standard"
[permissions]
workspace_write = true
# Outside-workspace writes are always classified as Ask first. "deny" is a
# stricter option that refuses them outright; "ask" lets the resolver decide.
outside_workspace = "ask"
shell_timeout_seconds = 120
# How an Ask is resolved: "human" (real approval prompt), "auto_approve"
# (record Ask -> resolved automatically), or "ai_review" (separate stateless
# model review for shell commands).
mode = "human"
[context]
# All context budgets are tokens. Bytes remain only for internal file,
# artifact, log, and I/O limits.
# recent_tokens = 64000 # verbatim transcript share of the request
# output_reserve_tokens = 8192 # reserved for the model's response
# reserve_tokens = 4000 # additional safety reserve
# max_request_tokens = 200000 # explicit cap; defaults to window - reserve
[failure]
retry_budget = 3
# Consecutive redundant inspection turns tolerated before the kernel
# re-grounds the model with the already-known unchanged observations.
stagnation_budget = 2
# Ultimate abnormal-behaviour circuit breaker. Unset means unlimited: long,
# productive tasks are never killed by turn count.
# max_model_turns = 1000
# Per-model metadata. The context window defaults to 256000 tokens when not
# configured; set it explicitly to match the provider model.
# [models.deepseek-flash]
# context_window_tokens = 262144
# Image input is explicit capability metadata. Built-in catalogs mark the
# official vision models, but a custom or newly released model can opt in
# without a Latch rebuild: input_modalities = ["text", "image"].
# input_modalities = ["text", "image"]
# Optional, user-configured pricing for the usage sidebar. Latch never fetches
# or invents prices; amounts are presented as estimates. Every component is
# optional, and an absent component stays unknown rather than zero.
# [models.deepseek-flash.pricing]
# input_per_million = 0.28
# output_per_million = 0.42
# cache_read_per_million = 0.028
# cache_write_per_million = 0.28
# currency = "USD"
# [[extensions]]
# name = "example"
# command = "node"
# args = ["/absolute/path/to/extensions/example-ts/dist/index.js"]
# Bounded extension lifecycle deadlines, in whole seconds. Every stage from
# spawn through exit is bounded, so a silent or broken extension can never
# block startup, RPC handling, or shutdown: a missed deadline kills and reaps
# the child. Unset fields keep the defaults shown.
# [extension_lifecycle]
# spawn_seconds = 5 # process spawn + initialize request write
# initialize_seconds = 10 # initialize response
# ready_seconds = 30 # registration stream until `ready`
# request_seconds = 120 # one tool/guard/transform/context response
# shutdown_seconds = 5 # shutdown response + exit notification
# exit_seconds = 5 # graceful exit before SIGKILL + reap
# Optional external MCP tool servers. These do not replace Latch extensions.
# Stdio always runs in the mandatory read-only extension sandbox.
# [[mcp_servers]]
# name = "local-tools"
# transport = "stdio"
# command = "python3"
# args = ["/absolute/path/to/server.py"]
# enabled = true
# timeout_seconds = 120
#
# [[mcp_servers]]
# name = "remote-tools"
# transport = "streamable_http"
# url = "https://example.com/mcp"
# bearer_token_env = "EXAMPLE_MCP_TOKEN"
# enabled = false
# timeout_seconds = 120
Edit this page on GitHub ↗Source: config.example.toml