Config¶
All config is read from environment variables with the LANGCLAW__ prefix and
__ as the nesting delimiter (e.g. LANGCLAW__BUS__BACKEND=rabbitmq). Provider
API keys are the exception — they use the provider's own plain env var
(ANTHROPIC_API_KEY, OPENAI_API_KEY, …), not a LANGCLAW__ key.
Typo'd keys are surfaced, not silently dropped
An unrecognized LANGCLAW__ env var (e.g. …__BOT_TOKEN when the field is
…__TOKEN) is logged as a WARNING at load time, naming the stray key. Set
LANGCLAW__STRICT_ENV=true to make an unknown key raise instead. Only
LANGCLAW__-prefixed keys are checked — plain provider vars are left alone.
Common environment variables¶
| Env var | Default | Notes |
|---|---|---|
LANGCLAW__STRICT_ENV |
false |
raise on an unknown LANGCLAW__ key instead of warning |
LANGCLAW__AGENTS__MODEL |
anthropic:claude-sonnet-4-5-20250929 |
provider:model spec |
LANGCLAW__AGENTS__RATE_LIMIT_RPM |
60 |
requests/min |
LANGCLAW__AGENTS__BACKEND__BACKEND |
local_shell |
local_shell (file tools + unsandboxed execute), filesystem (file tools only), state, store |
LANGCLAW__CHECKPOINTER__BACKEND |
sqlite |
sqlite | postgres |
LANGCLAW__CHECKPOINTER__SQLITE__DB_PATH |
~/.langclaw/state.db |
|
LANGCLAW__CHECKPOINTER__POSTGRES__DSN |
"" |
required for postgres (needs langclaw[postgres]) |
LANGCLAW__BUS__BACKEND |
asyncio |
asyncio | rabbitmq | kafka |
LANGCLAW__BUS__RABBITMQ__AMQP_URL |
amqp://guest:guest@localhost/ |
needs langclaw[rabbitmq] |
LANGCLAW__BUS__RABBITMQ__QUEUE_NAME |
langclaw.inbound |
|
LANGCLAW__BUS__KAFKA__BOOTSTRAP_SERVERS |
localhost:9092 |
needs langclaw[kafka] |
LANGCLAW__BUS__KAFKA__TOPIC |
langclaw.inbound |
|
LANGCLAW__PERMISSIONS__ENABLED |
false |
turn on RBAC |
LANGCLAW__PERMISSIONS__DEFAULT_ROLE |
viewer |
role for unlisted users |
LANGCLAW__CRON__ENABLED |
false |
sqlite store needs sqlalchemy+aiosqlite |
LANGCLAW__CRON__DATA_STORE__BACKEND |
sqlite |
separate from the checkpointer |
LANGCLAW__WORKFLOWS__ENABLED |
false |
|
LANGCLAW__WORKFLOWS__DURABLE_STEPS |
false |
memoize completed steps |
LANGCLAW__WORKFLOWS__RESUME_ON_STARTUP |
false |
re-drive interrupted runs (requires DURABLE_STEPS) |
LANGCLAW__INTERPRETER__ENABLED |
false |
sandboxed eval tool (needs langclaw[interpreter]) |
LANGCLAW__CHANNELS__TELEGRAM__ENABLED |
false |
|
LANGCLAW__CHANNELS__TELEGRAM__TOKEN |
"" |
bot token (needs langclaw[telegram]) |
LANGCLAW__CHANNELS__TELEGRAM__USER_ROLES |
{} |
id:role comma list, e.g. 123:admin,@alice:analyst |
LANGCLAW__CHANNELS__WEBSOCKET__ENABLED |
false |
bundled, no extra |
LANGCLAW__CHANNELS__WEBSOCKET__PORT |
18789 |
The same enabled / token / user_roles pattern applies to the Discord, Slack,
and Matrix channels. Full field-level docs (types and defaults) are rendered below.
Top-level config¶
langclaw.LangclawConfig
¶
Bases: BaseSettings
Root configuration object. Merges JSON file + env vars.
Environment variable format (double-underscore delimiter): LANGCLAW__AGENTS__MODEL=openai:gpt-4.1 LANGCLAW__BUS__BACKEND=rabbitmq
LLM provider keys use standard env vars (loaded from .env via
load_dotenv): ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
debug = False
class-attribute
instance-attribute
¶
When True, error responses sent back to the channel include a truncated
traceback (up to 500 characters) to aid debugging. Never enable in
production — tracebacks may expose internal paths and library details.
Override via env var: LANGCLAW__DEBUG=true.
log_level = 'WARNING'
class-attribute
instance-attribute
¶
Minimum log level for both stdlib logging and loguru.
Common values: "DEBUG", "INFO", "WARNING", "ERROR".
Override via env var: LANGCLAW__LOG_LEVEL=INFO.
strict_env = False
class-attribute
instance-attribute
¶
When True, an unknown LANGCLAW__ env var raises at load time instead
of logging a warning. Unknown keys are otherwise silently ignored by
pydantic-settings, which makes typos (e.g. …__BOT_TOKEN for …__TOKEN)
impossible to self-diagnose. Override via LANGCLAW__STRICT_ENV=true.
langclaw.load_config()
¶
Load and return the merged LangclawConfig.
Also calls load_dotenv() so that standard provider env vars
(ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) from .env
are available in os.environ for init_chat_model.
Unknown LANGCLAW__ env vars are surfaced via :func:validate_env_keys
(warning by default; raising when LANGCLAW__STRICT_ENV=true).
Source code in langclaw/config/schema.py
Sub-schemas¶
langclaw.config.schema.AgentConfig
¶
Bases: BaseModel
backend = Field(default_factory=BackendConfig)
class-attribute
instance-attribute
¶
Deepagents filesystem/shell backend selection. See BackendConfig.
workflows_dir
property
¶
Host directory holding runtime-authored (saved) workflow .js files.
Rooted at the agent's filesystem backend root so it matches where the
agent's own write_file lands: backend.root_dir when set, else the
workspace dir. (For state/store backends there is no host root and
file-authoring is unavailable — see WorkflowsConfig.)
langclaw.config.schema.BackendConfig
¶
Bases: BaseModel
Selects which deepagents filesystem backend the agent runs on.
deepagents abstracts the agent's file tools (ls/read_file/
write_file/edit_file/glob/grep and, for shell backends,
execute) behind a swappable backend. This config drives the common,
purely-declarative cases; advanced backends that need live objects
(StoreBackend with a custom store/namespace, CompositeBackend, a
sandbox, …) are injected as instances via Langclaw(backend=...) /
create_claw_agent(backend=...).
"local_shell"(default) — real files underroot_dirplus anexecutetool that runs shell commands on the host. Enables shell access;virtual_modestill sandboxes file paths toroot_dirbut theexecutecommand itself is unsandboxed (subprocesson the host). Disable by selecting another backend if that is too broad."filesystem"— real files underroot_dir, noexecutetool."state"— files live in LangGraph thread state (no host filesystem)."store"— files live in a LangGraphBaseStore(cross-thread).
backend = 'local_shell'
class-attribute
instance-attribute
¶
Which backend to construct. Default "local_shell" to expose the
execute tool.
env = Field(default_factory=dict)
class-attribute
instance-attribute
¶
local_shell only — extra environment variables for spawned commands.
execute_timeout = 120
class-attribute
instance-attribute
¶
local_shell only — per-command wall-clock timeout in seconds.
inherit_env = False
class-attribute
instance-attribute
¶
local_shell only — when True the host environment is inherited by
spawned commands. Off by default; combine with env for an explicit set.
max_output_bytes = 100000
class-attribute
instance-attribute
¶
local_shell only — cap on captured command output bytes.
root_dir = ''
class-attribute
instance-attribute
¶
Filesystem root for local_shell/filesystem backends. Empty means
'use the agent's workspace directory'. Ignored by state/store.
virtual_mode = True
class-attribute
instance-attribute
¶
Sandbox file paths to root_dir (reject ../ traversal). Applies to
the filesystem-rooted backends only.
langclaw.config.schema.ChannelsConfig
¶
Bases: BaseModel
langclaw.config.schema.TelegramChannelConfig
¶
Bases: BaseModel
streaming_enabled = False
class-attribute
instance-attribute
¶
Stream AI responses token-by-token by sending one message then editing it in place as new content arrives.
.. warning::
Enabling this may degrade reliability.
Telegram enforces a global rate limit of ~20 message edits per minute
per bot. Under moderate load (multiple concurrent users) this limit is
easily exceeded, causing RetryAfter errors and delayed delivery.
The 300 ms edit throttle reduces — but does not eliminate — the risk.
Enable only when the live-typing UX is more important than reliability,
and only in low-traffic environments. Leave disabled (default) to
receive the full response as a single message after generation completes.
Env: LANGCLAW__CHANNELS__TELEGRAM__STREAMING_ENABLED=true
user_roles = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Maps Telegram user IDs / @usernames to permission roles.
Env format: 123456:admin,@alice:editor
langclaw.config.schema.DiscordChannelConfig
¶
Bases: BaseModel
streaming_enabled = False
class-attribute
instance-attribute
¶
Stream AI responses token-by-token by sending one message then editing it in place as new content arrives.
.. warning::
Enabling this may degrade reliability.
Discord allows at most 5 edits per second per message and enforces a
global 50 req/s REST limit per bot. High-frequency edits during
generation can trigger 429 Too Many Requests errors, cause visible
lag, or result in dropped updates. The 300 ms throttle mitigates but
does not prevent this under concurrent load.
Enable only when the live-typing UX is more important than reliability,
and only in low-traffic environments. Leave disabled (default) to
receive the full response as a single message after generation completes.
Env: LANGCLAW__CHANNELS__DISCORD__STREAMING_ENABLED=true
user_roles = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Maps Discord user IDs to permission roles.
Env format: 123456:admin,789012:viewer
langclaw.config.schema.SlackChannelConfig
¶
Bases: BaseModel
app_token = ''
class-attribute
instance-attribute
¶
Slack App-Level Token for Socket Mode (starts with xapp-). Get from https://api.slack.com/apps -> Basic Information -> App-Level Tokens
bot_token = ''
class-attribute
instance-attribute
¶
Slack Bot User OAuth Token (starts with xoxb-). Get from https://api.slack.com/apps -> OAuth & Permissions
reaction_complete = 'white_check_mark'
class-attribute
instance-attribute
¶
Emoji name for 'complete' reaction. Default: 'white_check_mark' (✅).
reaction_feedback_enabled = True
class-attribute
instance-attribute
¶
Enable reaction emoji feedback (👀 while processing, ✅ when done).
reaction_processing = 'eyes'
class-attribute
instance-attribute
¶
Emoji name for 'processing' reaction. Default: 'eyes' (👀).
streaming_enabled = False
class-attribute
instance-attribute
¶
Stream AI responses token-by-token by posting one message then updating
it in place via chat_update as new content arrives.
.. warning::
Enabling this may degrade reliability.
Slack's chat_update API is Tier 3 (~50 req/min per app). Rapid
edits during generation can exhaust this quota, causing ratelimited
errors and stalled responses. The 300 ms update throttle reduces —
but does not eliminate — the risk, especially with multiple concurrent
users sharing the same bot quota.
Enable only when the live-typing UX is more important than reliability,
and only in low-traffic environments. Leave disabled (default) to
receive the full response as a single message after generation completes.
Env: LANGCLAW__CHANNELS__SLACK__STREAMING_ENABLED=true
user_roles = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Maps Slack user IDs to permission roles.
Env format: U123456:admin,U789012:viewer
langclaw.config.schema.MatrixChannelConfig
¶
Bases: BaseModel
access_token = ''
class-attribute
instance-attribute
¶
Long-lived access token obtained via /login or
matrix-commander --login. Starts with syt_ on Synapse.
allow_from = Field(default_factory=list)
class-attribute
instance-attribute
¶
Whitelist of Matrix user IDs, e.g. @alice:matrix.org,@bob:matrix.org.
Empty list means 'allow everyone'.
auto_join_invites = True
class-attribute
instance-attribute
¶
Automatically join rooms the bot is invited to. When allow_from
is non-empty the inviter must be on the allow-list.
device_id = ''
class-attribute
instance-attribute
¶
Device ID associated with access_token. Required by matrix-nio
for token-only authentication.
e2ee_enabled = False
class-attribute
instance-attribute
¶
Reserved for future support of end-to-end-encrypted rooms.
.. warning::
Not implemented in this release. Starting the channel with
e2ee_enabled=True raises an error at startup — the bot will
not silently fall back to unencrypted mode, which would leak
otherwise-encrypted messages on sync. Leave False for now;
set it to opt in once E2EE support ships.
homeserver_url = ''
class-attribute
instance-attribute
¶
Full homeserver URL, e.g. "https://matrix.org".
store_path = ''
class-attribute
instance-attribute
¶
Optional directory for nio's state store. Defaults to
~/.langclaw/matrix_store when left empty.
user_id = ''
class-attribute
instance-attribute
¶
Bot user ID in fully-qualified form, e.g. "@mybot:matrix.org".
user_roles = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Maps Matrix user IDs to permission roles.
Env format: @alice:matrix.org:admin,@bob:matrix.org:viewer
langclaw.config.schema.WebSocketChannelConfig
¶
Bases: BaseModel
streaming_enabled = True
class-attribute
instance-attribute
¶
Stream AI responses token-by-token, emitting {"type": "ai_chunk"}
events as content is generated, followed by {"type": "ai_stream_end"}.
Unlike Telegram, Slack, and Discord, WebSocket streaming carries no
rate-limit risk — chunks are pushed directly over the open socket without
any platform API calls. Clients should accumulate ai_chunk payloads
and render them incrementally.
Defaults to True. Set to False to receive a single
{"type": "ai"} event with the complete response instead.
Env: LANGCLAW__CHANNELS__WEBSOCKET__STREAMING_ENABLED=false
user_roles = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Maps WebSocket user IDs to permission roles.
Env format: user1:admin,user2:viewer
langclaw.config.schema.CheckpointerConfig
¶
Bases: BaseModel
langclaw.config.schema.BusConfig
¶
Bases: BaseModel
langclaw.config.schema.PermissionsConfig
¶
Bases: BaseModel
Global RBAC definitions.
Role definitions (role name -> allowed tools) live here.
User -> role mappings live per-channel alongside allow_from.
default_role = 'viewer'
class-attribute
instance-attribute
¶
Role assigned to users not listed in any channel's user_roles.
enabled = False
class-attribute
instance-attribute
¶
Enable per-user tool permission filtering.
roles = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Role name -> RoleConfig. Define in config.json::
{"roles": {"admin": {"tools": ["*"]}, "viewer": {"tools": ["web_search"]}}}
langclaw.config.schema.RoleConfig
¶
Bases: BaseModel
Defines which tools (and interpreter subagents) a role may use.
subagents = Field(default_factory=list)
class-attribute
instance-attribute
¶
Subagent types this role may invoke from an interpreter script via
tools.task({subagent_type}). Default-deny — an empty list means
the role cannot spawn any subagent from a script. Use ["*"] to allow
every registered subagent. This is a separate axis from tools: a role
with tools=["*"] still cannot reach subagents unless they are listed
here.
tools = Field(default_factory=list)
class-attribute
instance-attribute
¶
Tool names this role is allowed to invoke.
Use ["*"] to grant access to all tools.
workflows = Field(default_factory=list)
class-attribute
instance-attribute
¶
Workflow names this role may invoke — via the workflow_<name> tool,
a /workflows command, cron, or (Phase 2) tools.workflow.<name> inside
an interpreter script. Default-deny like subagents — an empty list
means the role may invoke no workflows. Use ["*"] to allow every
registered workflow. A third RBAC axis alongside tools and
subagents — all three resolve through the one descriptor-driven
:func:langclaw.rbac.resolve_capability (a new axis = one
:class:~langclaw.rbac.CapabilityAxis + one field here).
langclaw.config.schema.CronConfig
¶
Bases: BaseModel
enabled = False
class-attribute
instance-attribute
¶
Off by default. Enabling the SQLite data store (the default) requires the
sqlalchemy and aiosqlite packages. Set LANGCLAW__CRON__ENABLED=true
to opt in.
langclaw.config.schema.WorkflowsConfig
¶
Bases: BaseModel
Operator-authored Workflow primitive configuration (issue #38).
Opt-in and off by default. When enabled, workflows registered with
@app.workflow() become invocable three ways: the LLM calls the
workflow_<name> tool; an operator runs /workflows run <name>; or a
message with origin="workflow" (e.g. a cron-fired job) dispatches one
through the gateway. Each is typed, multi-step, and RBAC-gated by role.
Runtime authoring (mode="saved"): when this and interpreter are
enabled (and the backend is filesystem-rooted), the agent can save a workflow
by writing a file with its ordinary write_file tool — there is no
bespoke save tool. After running an ad-hoc job with eval, the user can say
"save that workflow"; the agent writes the same JS to workflows/<name>.js
(with // @description / // @uses header comments). The gateway watches
that folder, reconciles the file into the registry, and rebuilds the default
agent, so the new workflow_<name> tool goes live in the same session and
reloads on every restart. (Requires the interpreter extra — a saved body
runs in the same QuickJS sandbox as eval. state/store backends have
no host folder, so file-authoring is unavailable there.)
RBAC is enforced at the invocation boundary: the workflow_<name> tool
gate, the /workflows command, cron dispatch, and bus dispatch all consult
the role's default-deny workflow allowlist. A workflow's steps, however,
run in-process by calling tool.ainvoke directly — they bypass the
graph, so the per-request ToolPermissionMiddleware does not filter a
step's toolset. A workflow can therefore reach any tool in the default
agent's toolset; restrict reachable tools via the workflow's uses_tools,
not per-role tool RBAC. Bus dispatch runs a whole workflow as one bus
message; full bus → gateway re-entry per step (inheriting rate limiting,
channel context, per-step checkpointing, and step-level RBAC) is not yet
wired.
Env: LANGCLAW__WORKFLOWS__ENABLED=true
durable_steps = False
class-attribute
instance-attribute
¶
When True, completed workflow step results are persisted to a
LangGraph BaseStore (a sibling SQLite file or the Postgres DSN, matching
the checkpointer backend) instead of an in-process dict, so they survive a
process restart. Off by default.
NOTE: this only persists step results — it does not re-run anything on its
own. Set resume_on_startup for that. The store currently has no TTL or
pruning, so it grows unbounded; keep an eye on it for long-lived deployments.
enabled = False
class-attribute
instance-attribute
¶
Enable the Workflow primitive. Off by default — registering workflows is inert until this is set.
max_concurrent_runs = 16
class-attribute
instance-attribute
¶
Global ceiling on simultaneously-running workflow runs across the host, regardless of any single workflow's own budget.
max_depth = 2
class-attribute
instance-attribute
¶
Maximum nesting depth — how many levels a workflow may invoke other workflows. Bounds recursive fan-out.
max_steps_per_run = 1000
class-attribute
instance-attribute
¶
Hard backstop on total steps a single run may execute. Guards against a runaway loop in an operator-authored body.
resume_on_startup = False
class-attribute
instance-attribute
¶
When True, workflow runs left incomplete by a previous process (a crash
/ kill) are re-run on startup from the run journal: completed steps replay
from the durable step store and only the unfinished tail executes. Off by
default. Requires durable_steps (the step store + run journal share one
BaseStore); without it, enabling this logs a warning and does nothing.
Caveats developers should know before relying on it:
- Python-mode workflows only. Runs whose spec is no longer registered, or
whose
modeisllm_authored, are skipped (logged), not resumed. - Crash vs. clean failure. A killed process leaves a run
runningand so resumable; a workflow that raised a normal exception is markedfailedand is not retried (avoids looping on a deterministic bug). - No step-result invalidation. Resume matches cached steps by a
deterministic
step_id(<phase>#<seq>). Editing a workflow body between crash and restart can shift those IDs and replay stale results — bump the workflow name or clear the store after changing a body you may resume. - Resumes under the default agent's permissions. The resume step executor is built from the default agent's role-filtered toolset, not the original invoker's role/named-agent context — a resumed run may see a different toolset than the run that crashed.
- Blocking at startup. Incomplete runs are replayed sequentially before the gateway begins serving traffic, so a slow or hanging resumed run delays startup.
- One attempt. A run that raises again during resume is marked
failedand not retried — even if the cause was transient.
langclaw.config.schema.InterpreterConfig
¶
Bases: BaseModel
Sandboxed code-interpreter (RLM) configuration.
Opt-in and off by default. When enabled, the agent gains an eval
tool backed by langchain-quickjs's CodeInterpreterMiddleware: it
writes a sandboxed JavaScript program that can loop, branch, retry, and
fan out over a role-filtered allowlist of tools (Programmatic Tool Calling),
including tools.task({subagent_type}) to orchestrate registered
subagents.
The QuickJS sandbox has no filesystem, network, or shell access; the real
trust boundary is the exposed tools, so the PTC allowlist defaults to a
read-only set and mutating/egress tools require explicit allow_tools
opt-in. Requires the interpreter extra::
uv add 'langclaw[interpreter]'
Env: LANGCLAW__INTERPRETER__ENABLED=true
allow_tools = Field(default_factory=list)
class-attribute
instance-attribute
¶
Operator opt-in beyond the read-only default PTC allowlist. Add
mutating/egress tool names here to expose them inside scripts, or ["*"]
to expose every available tool (subject to per-role RBAC).
enabled = False
class-attribute
instance-attribute
¶
Enable the eval code-interpreter tool. Off by default.
max_concurrent_subagents = 4
class-attribute
instance-attribute
¶
Advisory cap on concurrent tools.task fan-out width.
.. note::
Reserved for forward compatibility — the current langchain-quickjs
CodeInterpreterMiddleware does not expose a concurrency knob, so
this value is documented but not yet enforced by the sandbox. The
effective bound today is max_ptc_calls plus timeout.
max_ptc_calls = 256
class-attribute
instance-attribute
¶
Maximum total tools.* bridge calls allowed during one eval.
Bounds runaway fan-out / PTC-call DoS.
max_result_chars = 4000
class-attribute
instance-attribute
¶
Caps what a script return (and captured console.log) sends back
to the agent turn.
memory_limit = 64 * 1024 * 1024
class-attribute
instance-attribute
¶
Bytes the QuickJS heap may use per eval. Default 64 MiB.
snapshot_between_turns = False
class-attribute
instance-attribute
¶
Persist REPL state across eval calls (snapshot after / restore before).
Off by default: a fresh context per eval avoids cross-call const/
let redeclaration errors when the model retries with the same variable
names. Enable only when a workflow genuinely needs to accumulate state
across separate eval calls.
timeout = 5.0
class-attribute
instance-attribute
¶
Per-eval wall-clock budget in seconds, including awaited
tools.task subagent runs.
langclaw.config.schema.ToolsConfig
¶
Bases: BaseModel
Configuration for built-in agent tools (web search, fetch, etc.).
brave_api_key = ''
class-attribute
instance-attribute
¶
Brave Search API key. Required when search_backend = "brave". Obtain one at https://api.search.brave.com/app/dashboard
gmail = Field(default_factory=GmailConfig)
class-attribute
instance-attribute
¶
Gmail tool configuration. See GmailConfig.
search_backend = 'brave'
class-attribute
instance-attribute
¶
Search backend to use. One of "brave", "tavily", or "duckduckgo".
tavily_api_key = ''
class-attribute
instance-attribute
¶
Tavily Search API key. Required when search_backend = "tavily". Obtain one at https://app.tavily.com