Configuration reference
August 8, 2026 · View on GitHub
Everything Bamboo reads from ${data_dir}/config.json (data_dir defaults to
${HOME}/.bamboo, override with BAMBOO_DATA_DIR or --data-dir), plus the
handful of sibling files and environment variables that participate in
configuration.
Prefer not to hand-edit JSON? bamboo init writes a starter config,
bamboo config set <dotted.key> <value> changes one value at a time (secret
keys are encrypted automatically — see Secrets below),
and bamboo config [--show-secrets] prints the resolved config. This document
is for when you need to know exactly what a key does or edit the file by hand.
Precedence: config.json < environment variables < CLI flags (bamboo serve --port ... wins over everything). Provider selection specifically is
providers.<name> / provider_instances.<id> (file) → BAMBOO_PROVIDER /
BAMBOO_<PROVIDER>_API_KEY (env, in-memory only, never persisted) →
--provider (CLI).
Source of truth for every struct below: crates/infra/bamboo-config/src/config.rs
(pub struct Config, around line 1069) unless noted otherwise. Struct field
lists here are derived directly from that code — if the two disagree, the code
wins; please file an issue.
- Top-level shape
- Providers
- Server
- Tools, skills, hooks
- LLM stream timeouts
- Memory / auto-dream / gardener
- Sub-agents + external CLI executors
- MCP servers
- Notifications
connect— the IM bridgeplugin_trust- Keyword masking
- Permissions
- Model limits (
model_limits.json) - Schedules (
schedules.json) - Environment variables
- Secrets and masking
- Encryption at rest
- Corrupt-config recovery
Top-level shape
{
"provider": "anthropic",
"providers": { "anthropic": { "api_key": "sk-ant-...", "model": "claude-sonnet-4-6" } },
"server": { "port": 9562, "bind": "127.0.0.1" }
}
Every top-level key is optional (#[serde(default)]) — a config with only
provider/providers is valid; every field below silently falls back to its
default. The full field list of Config:
| Field | Type | Notes |
|---|---|---|
http_proxy / https_proxy | String | Outbound proxy URLs for provider HTTP calls. |
proxy_auth_credential_ref | Option<String> | Stable reference to isolated proxy credentials (normally proxy.default.auth); no proxy plaintext, ciphertext, or mask is stored in ordinary config. |
provider | String | Default provider name. Default "anthropic". |
defaults | Option<DefaultsConfig> | Per-role model routing (chat/fast/vision/planning/...); only consulted when features.provider_model_ref is on. |
providers | ProviderConfigs | Legacy single-instance-per-type provider configs. See Providers. |
provider_instances | HashMap<String, ProviderInstanceConfig> | Multi-instance provider configs, keyed by an id you choose (e.g. two Anthropic keys under different labels). Takes precedence over providers when non-empty. |
default_provider_instance | Option<String> | Which provider_instances entry is the default; overrides legacy provider when set. |
server | ServerConfig | HTTP bind/port/TLS. See Server. |
keyword_masking | KeywordMaskingConfig | Outbound-body secret scrubbing. See Keyword masking. |
anthropic_model_mapping / gemini_model_mapping | { mappings: HashMap<String,String> } | Alias an OpenAI-shaped model id (e.g. "gemini-pro") to the real upstream model id for that provider's compat endpoint. |
hooks | HooksConfig | Request preflight hooks; today just image_fallback (text-only-model image handling). |
tools | ToolsConfig | { disabled: Vec<String> } — tool names omitted from every session's schema globally. |
skills | SkillsConfig | { disabled: Vec<String> } — skill ids excluded from selection/loading globally. |
env_vars | Vec<EnvVarEntry> | User-managed env vars injected into Bash-tool child processes. secret: true entries persist only stable credential_ref/configured metadata; their values live in the isolated credential store and are returned masked by the API. |
default_work_area | Option<DefaultWorkAreaConfig> | { path: Option<String> } — default workspace when a session has none set. |
access_control | Option<AccessControlConfig> | Password gate for the HTTP API/UI (password_enabled, hashed+salted). |
features | FeatureFlags | { provider_model_ref: bool, dynamic_model_routing: bool } — incremental rollout toggles, both off by default. |
stream_timeout | StreamTimeoutConfig | Independent transport, first-semantic, and midstream-semantic watchdog deadlines. See below. |
memory | Option<MemoryConfig> | Memory/auto-dream/gardener settings. See below. |
subagents | SubagentsConfig | Sub-agent execution + the claude_code executor. See below. |
cluster_fabric | ClusterFabricConfig | Operator-managed remote nodes for deploying broker-agent workers over SSH; empty by default. SSH secrets encrypted at rest. |
mcp (on-disk key mcpServers) | McpConfig | External tool servers. See MCP servers. |
notifications | NotificationsConfig | Desktop/ntfy/Bark delivery channels. See below. |
connect | ConnectConfig | Not actually stored here — see connect. |
plugin_trust | PluginTrustConfig | Plugin install trust policy. See below. |
extra | BTreeMap<String, Value> | Catch-all flatten for keys not (yet) promoted to a typed field — permissions, externalAgents, subagentRouting, setup-wizard state, etc. live here. Round-trips losslessly even for fields this version of Bamboo doesn't know about. |
Env variable writes use the dedicated revisioned /bamboo/env-vars API. Its
revision is the env-domain CAS revision stored in the credential envelope;
every semantic env change (including metadata, public values, ordering, and
deletes) advances it once, while a true no-op keeps it and emits no change
event. config.json participates in the same recoverable manifest transaction
and is hash-CAS protected. Secret
inputs use three states: omitted value keeps an existing secret, value: ""
explicitly clears it, and a non-empty value replaces it. Masks and client-sent
credential_ref/configured/value_encrypted fields are rejected. Existing
Lotus builds do not yet send this revision or omit value for metadata-only
edits; updating that client contract is deferred to the Lotus follow-up and is
not part of Bamboo Issue #597.
Providers
Two shapes coexist; a fresh bamboo init writes the legacy single-instance
providers shape, which is simplest for one key per provider:
{
"provider": "anthropic",
"providers": {
"anthropic": { "api_key": "sk-ant-...", "model": "claude-sonnet-4-6" }
}
}
Each provider stanza (OpenAIConfig / AnthropicConfig / GeminiConfig /
CopilotConfig / BodhiConfig, all in config.rs) shares this core shape —
api_key (write-only; persisted as api_key_encrypted, never re-emitted
plaintext by GET), base_url (override the upstream endpoint — self-hosted
proxies, Azure-style deployments, etc.), model, fast_model, vision_model,
reasoning_effort, responses_only_models: Vec<String> (force these models
onto the OpenAI Responses API path), request_overrides (provider-specific
per-endpoint HTTP header/body tweaks), and an extra flatten for
forward-compat fields. AnthropicConfig adds max_tokens and
thinking_replay_always (needed by some Anthropic-compatible upstreams, e.g.
GLM's /anthropic endpoint). BodhiConfig adds target_provider (which of
openai/anthropic/gemini the Bodhi proxy should present as). CopilotConfig
has no api_key at all — it authenticates via a cached OAuth token
(headless_auth for headless/CI login).
For GPT-5.6+ OpenAI Responses requests from the agent loop, Bamboo derives a
stable, session-scoped prompt_cache_key as a domain-separated SHA-256 hash.
The raw session identifier is never serialized into the provider request, and
non-agent requests do not receive a generated key. The key is only a cache
affinity hint that can improve routing to a matching prefix; it does not
guarantee a cache hit. request_overrides body patches run afterward, so an
operator may replace the generated key or remove prompt_cache_key entirely.
For more than one instance of a provider type (e.g. two separate Anthropic
keys/workspaces), use provider_instances instead:
{
"default_provider_instance": "work",
"provider_instances": {
"work": { "provider_type": "anthropic", "api_key": "sk-ant-work-...", "model": "claude-sonnet-4-6" },
"personal": { "provider_type": "anthropic", "api_key": "sk-ant-personal-...", "enabled": true }
}
}
provider_instances entries have the same field set as the legacy stanzas
plus provider_type (which of the five kinds this is) and enabled (default
true). When provider_instances is non-empty it takes precedence over
providers/provider as the routing source.
Server
{ "server": { "port": 9562, "bind": "127.0.0.1", "workers": 10 } }
port (default 9562), bind (default 127.0.0.1), static_dir (serve the
bundled frontend from a custom path), workers (Actix worker threads, default
10), tls: Option<TlsConfig> (cert_file/key_file PEM paths for manual
TLS termination — no ACME/auto-cert). All overridable per-invocation with
bamboo serve --port/--bind/--workers.
Tools, skills, hooks
tools.disabled: Vec<String>— tool names (e.g."Bash") hidden from every session's tool schema, globally. Compare to the SDK's per-agentAgentBuilder::tools([...]), which scopes selection to one in-processAgentinstead.skills.disabled: Vec<String>— skill ids excluded from selection/loading globally.hooks.image_fallback— how image parts are handled when the effective model/path is text-only (drop, OCR-replace, etc. — seeImageFallbackHookConfig).lifecycle_hooks— config-driven command or external.js/.py/.sh/.ps1/.batscript handlers for session, prompt, tool, compaction, and notification events. It lives inhooks.json; see the lifecycle hooks guide for runtime selection, the security model, and the input/output contract.
LLM stream timeouts
{
"stream_timeout": {
"transport_idle_timeout_secs": 120,
"first_semantic_timeout_secs": 600,
"semantic_idle_timeout_secs": 600
}
}
The three watchdogs measure different signals and apply identically to the main response stream and auxiliary silent model calls:
| Field | Default | Meaning |
|---|---|---|
transport_idle_timeout_secs | 120 | Maximum time for the provider call to establish its response stream, and the maximum subsequent gap between successfully received, non-empty response-body chunks. SSE ping/lifecycle events, comment heartbeats, and partial event fragments count even when they contain no token. |
first_semantic_timeout_secs | 600 | Maximum time from request dispatch to the first text, reasoning, or tool-call delta. Transport keepalives do not extend it. |
semantic_idle_timeout_secs | 600 | Maximum semantic-progress gap after output starts. Transport keepalives do not extend it. |
Every value must be between 1 and 86400 seconds. Invalid persisted values
are rejected by config loading; invalid values constructed by an embedding are
replaced with the safe defaults. Timeout errors report the expired phase,
deadline, provider/model identifiers, and last transport/semantic activity,
but never include prompts or raw provider payloads. A stream timeout is not
retried after text, reasoning, or tool-call output has started, because replay
could duplicate externally visible state. A timeout before any semantic output
on the primary response stream is marked retry-safe and may use the agent
loop's existing bounded turn retry policy. Auxiliary model calls are bounded by
the same watchdogs but never replay the containing agent turn.
OpenAI-compatible proxy heartbeats
An OpenAI-compatible proxy should establish the response stream within
transport_idle_timeout_secs, then either forward upstream response bytes or
emit an SSE heartbeat more frequently than that deadline. Bamboo treats any
successfully received, non-empty body chunk as transport activity before
parsing SSE, including the standard comment form : keep-alive\n\n. These
internal activity markers do not become model output and do not extend either
semantic deadline.
For example, CLIProxyAPI supports periodic streaming heartbeats with:
streaming:
keepalive-seconds: 15
CLIProxyAPI documents 0 (disabled) as the default. Operators using that or
another compatible proxy should choose a heartbeat interval safely below the
transport timeout. If a proxy cannot emit heartbeats during long upstream
reasoning gaps, configure transport_idle_timeout_secs at least as high as the
intended semantic wait instead; disabling the bounded transport watchdog is
not recommended.
Memory / auto-dream / gardener
Key memory (Option<MemoryConfig> — absent means every default below
applies). All the dream/gardener toggles default on; the values below are
the shipped defaults, so an empty {} is already reasonable:
| Field | Default | What it does |
|---|---|---|
background_model | None | Model used for memory extraction/consolidation background work; falls back to the primary model. |
summary_target_ratio | 0.20 | Desired durable conversation-summary size relative to the raw source tokens represented by it. Hierarchical reducers retain this global ratio instead of applying it again at every level. |
summary_safe_window_percent | 80 | Maximum share of the summarization model's total context window used by each fully rendered map/reduce request, including requested output and the tokenizer safety margin. |
auto_dream_enabled | true | Distill conversation stretches into candidate memories + notebook entries as the session runs. |
auto_dream_interval_secs | 1800 | How often the dream pass runs. |
project_prompt_injection | true | Inject relevant project-scoped memory into the system prompt. |
relevant_recall | true | Retrieve relevant durable memories for the current turn. |
relevant_recall_rerank | false | Rerank recalled memories (extra model call) before injecting. |
project_first_dream | true | Prefer project-scoped memory on a session's first dream pass. |
ledger_agenda_injection / ledger_gardener_enabled / ledger_distillation_enabled | true | Personal-assistant ledger subsystem toggles. |
ledger_gardener_interval_secs | 21600 (6h) | Ledger gardener cadence. |
gardener_enabled | true | Background job that splits "multi-topic blob" memories; calls no LLM when its deterministic pre-screen finds no candidates. |
gardener_interval_secs | 86400 (daily) | Gardener cadence. |
gardener_volume_trigger | 25 | Run early once this many new memories have accrued, instead of waiting for the interval. |
gardener_max_splits_per_run / gardener_min_sections | 8 / 5 | Cost guardrails on one gardener pass. |
dedup_gardener_enabled | true | Background near-duplicate memory merge pass. |
dedup_gardener_min_score | 0.6 | Jaccard similarity threshold to merge. |
dedup_gardener_max_merges_per_run | 8 | Cap per pass. |
memory_active_capacity | 0 (unbounded/off) | Cap on "active" memory count before older ones archive. |
capacity_max_archivals_per_run | 50 | Cap per capacity-enforcement pass. |
granularity_freshness_gardener_enabled | true | Background staleness/granularity pass. |
Automatic conversation compression always maps bounded source chunks and then
reduces their summaries, even when the selected source would fit in one model
request. Large terminal results are reduced into bounded multipart sections;
the persisted summary keeps the single overall summary_target_ratio budget.
project_prompt_injection / relevant_recall / relevant_recall_rerank /
project_first_dream can also be flipped via env vars — see
Environment variables — which is handy for a
one-off container run without touching config.json.
auto_dream_enabled/gardener_enabled intentionally consume model tokens
when on; turn them off ({"memory": {"auto_dream_enabled": false}}) for a
minimal-cost deployment.
Sub-agents + external CLI executors
Key subagents (SubagentsConfig). Sub-agents always run as independent
actor subprocesses (crash isolation + real parallelism) — there is no
in-process runtime toggle.
| Field | Purpose |
|---|---|
max_concurrent | Cap on simultaneously running sub-agents. |
worker_bin / worker_args | Override the sub-agent worker binary/args (defaults to the current bamboo binary's subagent-worker mode). |
fabric_dir | Where the actor fabric's mailbox/state files live. |
executor | Which executor spawns a child: "echo" (test stub) | "bamboo_runtime" (default — a full nested Bamboo agent loop) | "claude_code" | "codex". |
claude_code_binary | Path to the claude binary; None resolves claude via PATH. |
claude_code_model | --model passed to claude. |
claude_code_permission_mode | --permission-mode passed to claude (always sent explicitly, even "default", once this executor is selected). |
claude_code_inherit_user_config | false/unset adds --strict-mcp-config --setting-sources project, sandboxing the child from your personal claude config. |
claude_code_forward_env | Extra environment variable names forwarded verbatim into the child (on top of a fixed allowlist: HOME/PATH/SHELL/TERM/LANG/LC_*/TMPDIR/USER/LOGNAME). |
codex_binary / codex_model | Codex executable and optional --model override. |
codex_mode | "exec" (default, one process per activation) or "app_server" (long-lived JSON-RPC with parent approval relay). Missing app-server capability fails clearly and never downgrades. |
codex_auth_mode | "inherit" | "api_key" | "custom" | "bamboo"; unset defaults to the recommended "bamboo" parent-provider mode. |
codex_base_url | Absolute HTTP(S) URL for custom mode only; credentials, query parameters, and fragments are rejected. |
codex_wire_api | "responses" (the only protocol accepted by supported Codex CLI versions). |
codex_provider_key_ref | Existing Bamboo provider credential reference used only by custom mode; the key is injected through an environment variable and is not written to the generated Codex config. |
codex_forward_env | Extra environment names after env_clear(); api_key mode requires an explicit OPENAI_API_KEY, and other modes reject it. CODEX_* and Bamboo's managed provider-key variable are reserved. |
codex_sandbox | Optional explicit "read-only" | "workspace-write" | "danger-full-access". Unset derives a safe value from the child profile and live parent bypass posture. |
codex_approval_policy | Exec mode accepts optional "never" | "on-failure"; app-server mode accepts unset or "on-request". Cross-mode combinations are rejected. |
codex_network_access | Enables network access inside workspace-write; incompatible with an explicit read-only sandbox. |
codex_allow_danger_bypass | Second gate for disabling the OS sandbox. The live parent must also be in bypass mode; root workers always downgrade and warn. |
remote_placements / schedulable_placements | Where a sub-agent may run (local / a named Cluster Fabric node) and whether schedules may target it. |
mcp_role_allowlist | Restrict which MCP servers a sub-agent role may see. |
{
"subagents": {
"executor": "claude_code",
"claude_code_model": "claude-sonnet-4-6",
"claude_code_permission_mode": "acceptEdits",
"claude_code_forward_env": ["MY_TOOL_TOKEN"]
}
}
The same claude_code_* and codex_* field sets are duplicated per-agent under
ExternalAgentProfile (Config.extra["externalAgents"],
bamboo-engine/src/external_agents/config.rs) when you need different
external CLI executor settings for different named agents rather than one
global default.
The concrete spawn implementation is src/claude_code_executor.rs
(ClaudeCodeExecutor): it runs claude --output-format stream-json --input-format stream-json --permission-prompt-tool stdio --replay-user-messages --verbose [--model ...] [--permission-mode ...] [--resume <id>], in a fully env_clear()'d child process (only the allowlist
above is passed through) — the session id maps to claude's own --resume
via a small claude-code-session.json state file per sub-agent workspace.
For Codex configuration, billing implications, isolation details, and the
per-run Bamboo token contract, see codex-executor.md.
Lotus validates this section with POST /bamboo/config/validate; its binary
Detect action calls POST /bamboo/config/codex/detect, which returns the
resolved path and version only after the same preflight used at worker
spawn.
MCP servers
On-disk key mcpServers (legacy mcp alias still read), typed field
Config.mcp: McpConfig:
{
"mcpServers": {
"version": 1,
"servers": [
{
"id": "filesystem",
"name": "Local filesystem",
"enabled": true,
"transport": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allow"]
},
"request_timeout_ms": 60000,
"healthcheck_interval_ms": 30000,
"allowed_tools": [],
"denied_tools": []
}
]
}
}
transport is one of three shapes (tagged by type): stdio
(command/args/cwd/env/startup_timeout_ms — spawns a child process),
sse (url/headers/connect_timeout_ms), or streamable_http (same shape
as sse, MCP's newer single-endpoint transport). reconnect controls
auto-reconnect backoff (enabled, initial_backoff_ms, max_backoff_ms,
max_attempts, 0 = unlimited). allowed_tools/denied_tools filter which of
the server's advertised tools are actually exposed (empty allowed_tools =
all allowed). See the bamboo mcp CLI verbs
for managing this without hand-editing JSON, or
examples/mcp_client.rs for wiring a server
programmatically via the SDK.
Notifications
Key notifications (NotificationsConfig):
{
"notifications": {
"desktop": { "enabled": true },
"ntfy": { "enabled": true, "base_url": "https://ntfy.sh", "topic": "my-bamboo-alerts", "credential_ref": "notification.ntfy.token", "configured": true },
"bark": { "enabled": false, "base_url": "https://api.day.app", "credential_ref": "notification.bark.device_key", "configured": false }
}
}
desktop.enabled: Option<bool> — None auto-detects (on for a standalone
bamboo serve, off when running under a --parent-pid sidecar, since the
host app usually owns notifications there). ntfy/bark are push-relay
channels; ntfy.token/bark.device_key live only in the isolated encrypted
credential store. Ordinary config.json and parseable rotated backups contain
only stable credential_ref/configured metadata, never plaintext,
ciphertext, or a UI mask. Legacy plaintext/ciphertext is migrated idempotently
through the recoverable config/credential manifest.
GET /bamboo/config/notifications returns the current credential revision,
health, source, channel metadata, and per-channel configured/source/update
status without a secret slot. Notification updates use POST /bamboo/config
with only expected_revision and notifications in the request. Omitting a
secret keeps it, null or "" clears it, and a non-empty string replaces it;
masks and client-supplied credential_ref/configured/ciphertext are rejected.
Notification changes cannot be combined with another root domain in one call.
"notifications": null is an explicit domain reset: both credentials are
cleared and notification metadata returns to defaults in the same transaction.
bamboo config set notifications.ntfy.token ... and the Bark equivalent route
through the same manifest transaction.
All three channels feed the same
AgentEvent::Notification category/priority policy — see
crates/infra/bamboo-notification.
connect — the IM bridge
Drives sessions from IM platforms (Telegram, Feishu/Lark). Despite Config
having a typed connect field, this is NOT stored in config.json — it
lives in its own sibling file, ${data_dir}/connect.json, loaded/merged by
Config::merge_connect_config and saved by Config::save_connect_config
(both in config.rs). A config.json with no connect.json next to it and
no legacy inline connect key starts zero background tasks — fully inert
by default.
{
"platforms": [
{
"id": "b3f5...",
"type": "telegram",
"token": "123456:ABC-DEF...",
"allow_from": ["123456789"],
"admin_from": []
}
]
}
Fields (ConnectPlatformConfig): id (stable UUID, auto-backfilled on save —
never assume it's present on a hand-written entry), type ("telegram" |
"feishu"; unrecognized values are skipped with a startup warning, not a hard
failure), token/token_encrypted (bot token, Telegram), app_id (Feishu,
not a secret), app_secret/app_secret_encrypted (Feishu), domain (Feishu
only — None/"feishu" → open.feishu.cn, "lark" → open.larksuite.com,
or an explicit https:// base for self-hosted deployments), allow_from
(empty = deny-all — deliberately stricter default than other allowlists in
this codebase, since IM bridges are internet-facing by nature), admin_from
(parsed, currently unused).
A legacy inline connect key found inside config.json (from before this was
split out) is migrated automatically on next load: adopted into
connect.json, then stripped from config.json. A corrupt connect.json is
quarantined to connect.json.bak and treated as empty (fail-safe — never
silently falls back to a stale inline copy).
plugin_trust
Key plugin_trust (PluginTrustConfig) — the trust policy for bamboo plugin install <url> (see Plugins how-to):
{
"plugin_trust": {
"trusted_hosts": ["github.com/bigduu/"],
"trusted_keys": [
{ "label": "nova official", "algorithm": "ed25519", "public_key": "<hex>" }
],
"enforcement": "strict"
}
}
trusted_hosts — host+path prefixes a url-source install's URL must match
to skip --allow-untrusted-host. trusted_keys — ed25519 public keys (hex)
trusted to sign plugin bundles (defaults ship the official nova + magpie
keys); a bundle signed by one of these skips --allow-unsigned AND, per the
trust model, also satisfies the checksum requirement (a verified signature is
strictly stronger than a pasted sha256). enforcement — "strict"
(default) or "off" (accepts a bool too: true==strict, false==off);
"off" is the config-level equivalent of passing --insecure to every url
install, for a private/dev instance that never wants confirmation prompts.
Local (local_dir/local_archive) installs are never subject to this policy
— it only gates network downloads.
Keyword masking
Key keyword_masking (KeywordMaskingConfig { entries: Vec<KeywordEntry> },
each { pattern, match_type: "exact" | "regex", enabled }). Applied as a
value-aware scan over the FINAL serialized outbound provider request body
(not field-by-field) — every string value matching a pattern is masked before
the request leaves the process, catching secrets that end up embedded in tool
output, file contents, etc., not just ones typed directly into chat.
Permissions
Lives under the "permissions" key inside Config.extra (the flatten
catch-all — not yet promoted to a typed top-level field). Shape
(SerializablePermissionConfig, crates/infra/bamboo-permission/src/config.rs):
whitelist: Vec<PermissionRule>, enabled: bool, session_grant_duration_secs
(default 1800), mode: Option<PermissionMode>, confirm_threshold: Option<RiskLevel>, ask_rules: Vec<String> — glob-ish patterns like
"Bash(rm -rf *)" that force a confirmation prompt even under the legacy
bypassPermissions mode. The design invariant: bypass skips ordinary prompts
but still asks for the user's own ask_rules and a small hard-coded set of
catastrophic commands (sudo, curl | sh, dd, rm -rf /, …). The stronger
auto mode emits no approval prompt, including for those forced-ask cases, but
still enforces explicit policy and platform denials.
Model limits (model_limits.json)
A separate file, ${data_dir}/model_limits.json — user-supplied
context/output token limit overrides. Explicit user matches take precedence
over provider runtime metadata. The legacy standalone representation is a raw
array:
[
{ "model_pattern": "my-custom-model", "max_context_tokens": 136192, "max_output_tokens": 8192 }
]
model_pattern is either an exact model id or a literal substring of the
runtime model id; * and other glob characters have no special meaning.
Among substring matches, the longest pattern wins.
max_context_tokens is the provider's total input + output context
window, not its input allowance alone. Bamboo derives the per-request input
limit as:
max_request_input_tokens =
max_context_tokens - max_output_tokens - safety_margin
safety_margin is optional. When max_output_tokens is omitted, Bamboo
derives it from the context window. Provider metadata that exposes separate
max_input_tokens and max_output_tokens is normalized to the same total
context-window contract before runtime budgeting.
The modular configuration store persists this section in a revisioned
{schema_version, revision, data} envelope. Runtime loading accepts both that
envelope and the legacy raw array, and root sessions re-read the sidecar at
the start of every agent round; an explicit session/child or engine-level
TokenBudget remains an intentional higher-priority override. Manage the
section through Bamboo's settings API instead of hand-editing it while the
server is running.
With no matching user or provider value, Bamboo falls back to a global default of 1M total context / 128K output. There is deliberately no built-in per-model table, so stale hard-coded model names cannot override live provider metadata.
Schedules (schedules.json)
Also a separate file, ${data_dir}/schedules.json (not part of
config.json) — timed/cron tasks managed by bamboo schedules list|show|create|delete|run|runs or the /bamboo/schedules HTTP routes. Each
entry (ScheduleSpec) has an id/name/enabled, a trigger (Interval
| Once | Daily | Weekly | Monthly | Cron), an optional
timezone, start_at/end_at bounds, a misfire_policy (what happens if
the process was down when a fire was due: RunOnce (default) | Skip |
CatchUpAll | CatchUpWindow), an overlap_policy (Allow | Skip |
QueueOne, default QueueOne), and run_config (the prompt/session
parameters for the fired run). Not meant for hand-editing — use the CLI/HTTP
verbs, which validate the trigger shape.
Environment variables
Every BAMBOO_* variable Bamboo reads, grouped by what it affects. All are
optional; file config plus built-in defaults cover a fresh install.
Bootstrapping / core:
| Var | Effect |
|---|---|
BAMBOO_DATA_DIR | Data directory (default ${HOME}/.bamboo). |
BAMBOO_PORT | Server port override. |
BAMBOO_BIND | Server bind address override. |
BAMBOO_PROVIDER | Default provider override. |
BAMBOO_HEADLESS | Enable headless auth mode. |
BAMBOO_WORKERS | Actix worker-count override (CLI-level). |
Provider API keys (in-memory only — never persisted to config.json,
even after bamboo config set; the point is a plaintext-key-free config file
for Docker/CI/secret-manager deploys):
BAMBOO_OPENAI_API_KEY, BAMBOO_ANTHROPIC_API_KEY, BAMBOO_GEMINI_API_KEY.
Memory toggles (override the matching memory.* config field):
BAMBOO_MEMORY_PROJECT_PROMPT_INJECTION, BAMBOO_MEMORY_RELEVANT_RECALL,
BAMBOO_MEMORY_RELEVANT_RECALL_RERANK, BAMBOO_MEMORY_PROJECT_FIRST_DREAM.
Server hardening / networking:
| Var | Effect |
|---|---|
BAMBOO_RATE_LIMIT_PER_SECOND / BAMBOO_RATE_LIMIT_BURST | Governor rate-limiter tuning. |
BAMBOO_RATE_LIMIT_TRUST_XFF / BAMBOO_RATE_LIMIT_TRUSTED_HOPS | Trust X-Forwarded-For behind N reverse-proxy hops. |
BAMBOO_CSP | Full Content-Security-Policy header override. |
BAMBOO_CSP_CONNECT_SRC | Just the CSP connect-src directive. |
BAMBOO_CORS_ALLOW_ORIGINS | CORS allowlist. |
BAMBOO_ENABLE_DEV_ENDPOINTS | Gate dev-only HTTP endpoints. |
BAMBOO_WS_AUTH_DEADLINE_MS | WS v2 auth handshake timeout. |
BAMBOO_WEB_SEARCH_ENDPOINTS | Ordered, comma-separated absolute HTTP(S) endpoints used by WebSearch. Each endpoint receives a POST form with the q field; the first recognized HTML/Lite response wins. Defaults to DuckDuckGo's HTML endpoint followed by its Lite endpoint. |
Workspace / paths:
| Var | Effect |
|---|---|
BAMBOO_WORKSPACE_DIR | Project/workspace directory override. |
BAMBOO_WORKSPACE_ROOT | Root dir for session workspaces (default {data_dir}/workspaces). |
BAMBOO_WORKSPACE_CONFINE | 1/true/yes forces workspace paths to stay under BAMBOO_WORKSPACE_ROOT; implied when that var is set. |
BAMBOO_SKILL_MODE | Active skill mode override. |
Provider/runtime tuning:
| Var | Effect |
|---|---|
BAMBOO_LLM_MAX_RETRIES / BAMBOO_LLM_RETRY_BASE_DELAY_MS / BAMBOO_LLM_RETRY_MAX_DELAY_MS | LLM HTTP request retry policy. |
BAMBOO_RESPONSES_DEBUG / BAMBOO_RESPONSES_DEBUG_FILE | Dump raw OpenAI Responses API traffic to a file for debugging. |
BAMBOO_JS_REPL_NODE_PATH | Node binary used by the js_repl tool. |
BAMBOO_PYTHON | Python interpreter override. |
Windows-specific: BAMBOO_WINDOWS_BASH_PATH, BAMBOO_WINDOWS_CMD_TRACE
(also honors BODHI_WINDOWS_CMD_TRACE).
Secrets / plugins / broker:
| Var | Effect |
|---|---|
BAMBOO_CONFIG_ENCRYPTION_KEY | Master AES-256 key for at-rest secret encryption — see Encryption at rest. |
BAMBOO_BROKER_TOKEN | Auth token for bamboo broker/broker-agent subcommands. |
BAMBOO_PLUGIN_SERVICE_CONFIG | Config path passed into a plugin service's own subprocess. |
BAMBOO_FRONTEND_PACKAGE | Override the bundled frontend static package path. |
Everything above is read via plain std::env::var, so it can also be set
through your process manager / Docker Compose / systemd unit rather than
exported in a shell.
Secrets and masking
Every secret field (providers.*.api_key, provider_instances.*.api_key,
notifications.ntfy.token, notifications.bark.device_key,
connect.platforms[].token/.app_secret, subagents.broker.token,
cluster_fabric node SSH credentials, secret env_vars entries) follows one
contract everywhere it's read or written:
-
Read (
GET/bamboo config): a configured secret is never echoed back plaintext. It's replaced with exactly the literal string****...****; if the field isn't configured at all, the key is omitted entirely (not sent as""). -
Write (
PATCH/bamboo config set): a submitted value counts as "keep the existing secret unchanged" if and only if, after trimming, it consists entirely of*and/or.characters — i.e. it matches the masked placeholder shape exactly. This is a whole-value check, not a substring check:is_masked_api_key()incrates/infra/bamboo-config/src/patch.rs. An empty string explicitly clears the secret. Anything else — including a string that still starts with the placeholder because a UI's prefill wasn't fully cleared before pasting (e.g.****...****sk-newkey123) — is treated as a real new secret and applied.This whole-value rule is deliberate: an earlier substring-based check (fixed as issue #430) could silently discard a user's pasted key when the placeholder wasn't fully selected/overwritten first. Any client embedding Bamboo's settings UI must never pre-fill an editable secret field with the masked placeholder — leave it blank to mean "keep."
Encryption at rest
Every *_encrypted field uses AES-256-GCM (crates/infra/bamboo-config/src/encryption.rs);
on disk the ciphertext is stored as hex(nonce):hex(ciphertext), a fresh
random nonce per encryption. The master key is resolved once per process, in
priority order:
BAMBOO_CONFIG_ENCRYPTION_KEY— hex-encoded, must decode to exactly 32 bytes. Highest priority; use this for reproducible/ephemeral deployments (containers, CI) where you manage the key externally.- Key file
${data_dir}/.bamboo_encryption_key— hex-encoded 32 bytes, written with0600permissions atomically on Unix. - Machine-derived key — SHA-256 of a machine identifier (
/etc/machine-idon Linux, registryMachineGuidon Windows,ioreg IOPlatformUUIDon macOS) with domain separation, then persisted to the key file so subsequent runs don't re-derive it. - Last resort — cryptographically random 32 bytes, persisted to the key file.
Backup/disaster-recovery implication: losing the key file on a host with
no stable machine identifier (and no BAMBOO_CONFIG_ENCRYPTION_KEY set) makes
every *_encrypted field in that data directory permanently undecryptable —
back up .bamboo_encryption_key alongside config.json if you back up your
Bamboo data directory at all.
Corrupt-config recovery
If ${data_dir}/config.json exists but fails to parse, Config::from_data_dir
does not crash or silently reset to defaults — it runs a recovery flow
(issue #493 and predecessors), roughly:
- Quarantine the unparseable original by copying it to
config.json.corrupted.<timestamp>(the corrupt file is never deleted or moved — the original stays atconfig.jsonuntouched). - Recover, trying strategies in order:
- Salvage — parse the corrupt file as generic JSON and adopt each
top-level key individually onto the best available baseline, keeping a
key only if the whole
Configstill deserializes with it applied. - Backup — if salvage found nothing usable, fall back to
config.json.bak,.bak.1,.bak.2(newest first; 3 generations are kept, rotated on every successful save). - Defaults — if neither works, a fresh default
Config.
- Salvage — parse the corrupt file as generic JSON and adopt each
top-level key individually onto the best available baseline, keeping a
key only if the whole
- The recovered config is tagged in memory (
recovery_status, never persisted) with which strategy produced it and which fields were salvaged. - The recovered config is never auto-saved.
Config::save_to_dirrefuses to write while recovery is unconfirmed, so the quarantined corrupt original on disk is preserved until something explicitly callsConfig::confirm_recovery()/confirm_recovery_and_save_to_dir()(the settings UI/CLI does this after showing the user what was recovered).
Net effect: a corrupted config.json never causes silent data loss — you
always get either your own values back (salvage/backup) or an explicit,
confirmable prompt before anything is overwritten.