Skip to content

nimblebrain.json

NimbleBrain splits configuration into two files:

  • nimblebrain.json — instance-level settings (models, HTTP, logging, limits, feature flags). One file per deployment. This page.
  • workspace.json — per-workspace settings (bundles, skill directories, named agent profiles, optional model + identity overrides). See workspace.json.

Top-level bundles, skillDirs, agents, home, and preferences entries in nimblebrain.json are silently stripped at load — they belong in workspace.json now. See workspace configuration for where they live.

NimbleBrain looks for the config file in this order, stopping at the first match:

  1. --config <path> — Explicit path passed as a CLI flag.
  2. .nimblebrain/nimblebrain.json — Project-local config directory in the current working directory.
  3. <defaultWorkDir>/nimblebrain.json — Inside the command’s default working directory (~/.nimblebrain for bun run start, the project-local workdir for worktree dev).
  4. ./nimblebrain.json — Current working directory (bare file).

If no config file exists at the resolved path, NimbleBrain auto-creates a minimal default file ({ "$schema": …, "version": "1" }) as long as the parent directory exists.

A sibling nimblebrain.overrides.json next to the resolved config file is deep-merged on top of it at load. The seed (nimblebrain.json) is operator-managed and overwritten on every deploy; the override is written by the admin set_model_config tool and preserved across deploys. The merge is one level deep — top-level objects (e.g. models) are merged key-by-key so an override of models.fast doesn’t clobber the seed’s models.default; scalars and arrays are replaced wholesale. A malformed override file is logged loudly and skipped (the seed still boots) rather than taking down startup.

{
"$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json",
"version": "1",
"models": {
"default": "anthropic:claude-sonnet-4-6",
"fast": "anthropic:claude-haiku-4-5-20251001"
},
"providers": {
"anthropic": { "apiKey": "sk-ant-..." },
"openai": { "apiKey": "sk-..." }
},
"maxIterations": 25,
"maxInputTokens": 500000,
"maxOutputTokens": 16384,
"maxToolResultSize": 1000000,
"logging": { "dir": "~/.nimblebrain/logs", "disabled": false, "level": "normal", "retentionDays": 30 },
"http": { "port": 27247, "host": "127.0.0.1" },
"features": {
"bundleManagement": true,
"skillManagement": true,
"delegation": true,
"toolDiscovery": true,
"bundleDiscovery": true,
"fileContext": true,
"userManagement": true,
"workspaceManagement": true,
"compaction": true
},
"connectors": {
"providers": {
"composio": { "monitorEnabled": true }
}
},
"files": {
"maxFileSize": 26214400,
"maxTotalSize": 104857600,
"maxFilesPerMessage": 10,
"maxExtractedTextSize": 204800
},
"allowInsecureRemotes": false,
"telemetry": { "enabled": true },
"workDir": "~/.nimblebrain"
}
FieldTypeDefaultDescription
$schemastringJSON Schema URI. Set to "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json" for editor autocompletion.
versionstringConfig file version. Must be "1".
modelsobjectRole-based model slots. See models.
modelPolicyobjectWhich models this organization permits. See modelPolicy.
providersobjectPer-provider API keys. See providers.
modelobject{ "provider": "anthropic" }Legacy single-provider config. Use providers instead.
defaultModelstringDeprecated. Use models.default instead.
maxIterationsinteger25Max agentic iterations per request. Range: 1—50.
maxInputTokensinteger500000Max input tokens per request. Engine stops when this budget is exceeded.
maxOutputTokensinteger16384Max output tokens per LLM call.
maxToolResultSizeinteger1000000Max characters for a single tool result. 0 disables the cap.
thinking"off" | "adaptive" | "enabled"enabled on reasoning modelsExtended-thinking mode, applied to every provider that supports reasoning. off: do not reason — not enforceable on Anthropic’s adaptive-only models (Opus 4.7/4.8, Sonnet 5, Opus 5), which have no such state: the AI SDK validates thinking.type=disabled and then never sends it, so the model applies its own default (#738). adaptive: model decides per call, at no stated depth. enabled: reason every turn, at thinkingEffort. Applies only to models the catalog carries and flags reasoning-capable — an override can ask for reasoning but cannot make the parameter exist. A pinned model id, or one served by an OpenAI-compatible proxy under its own name, isn’t in the catalog and so gets no reasoning options; the runtime logs a warning naming the model when it drops an override this way. Typically set via the admin set_model_config tool, which writes it to nimblebrain.overrides.json.
thinkingEffort"low" | "medium" | "high" | "xhigh" | "max"mediumHow hard to think when reasoning is on. The portable control — see reasoning depth for how it reaches each provider. Ignored for off and adaptive.
thinkingBudgetTokensintegerExplicit token budget, for metering thinking in tokens instead of naming a depth. Only honored by providers that meter it that way (Anthropic up to 4.6, Gemini 2.5); elsewhere thinkingEffort applies — Gemini 3 takes a level, not a budget. Counts toward maxOutputTokens. Anthropic requires a minimum of 1,024. Prefer thinkingEffort unless you need a hard token cap.
sessionStoreobject{ "type": "memory", "ttlSeconds": 28800 }MCP session metadata store for /mcp. See sessionStore.
loggingobjectStructured logging config. See logging.
httpobject{ "port": 27247, "host": "127.0.0.1" }HTTP server config. Omit for programmatic-only use.
featuresobjectall trueFeature flags. See Feature Flags.
connectorsobjectManaged-connector provider settings (Composio). See connectors.
filesobjectFile upload limits. See files.
allowInsecureRemotesbooleanfalseAllow HTTP (non-TLS) remote bundle connections. Dev only.
telemetryobject{ "enabled": true }Anonymous usage telemetry. See telemetry.
workDirstring"~/.nimblebrain"Working directory for runtime state (conversations, logs, workspaces).

Providers disagree about how to ask a model to think harder. Two take a named effort tier; two meter it in tokens. NimbleBrain’s config is the effort tier, and the runtime translates.

ProviderWhat it receives
Anthropic — Opus 4.7/4.8, Sonnet 5, Opus 5thinking: {type: "adaptive"} + output_config.effort, the tier verbatim
Anthropic — up to Sonnet 4.6thinking: {type: "enabled"} + a budgetTokens sized from the tier
OpenAI, and Nebius-hosted open-weight models (Qwen, Kimi, GLM, gpt-oss, Cosmos3)reasoningEffort. Its usable range stops at high, so xhigh and max both land there. On OpenAI, acceptance is also per model — see below
xAI (Grok)reasoningEffort, tops out at high. Acceptance is per model and some Grok models take no effort parameter at all — see below
Google Gemini 3thinkingConfig.thinkingLevel, tops out at high
Google Gemini 2.5thinkingConfig.thinkingBudget, sized from the tier and held inside the model’s documented range

The translation only runs one way. A depth is converted into a token budget where a provider needs one, because the model’s output ceiling is a reasonable scale to express a chosen depth against. The reverse is not done: a token budget carries no depth to recover, and deriving one from a ceiling the operator never set would turn a number nobody chose into an instruction to reason as hard as possible on every call.

thinkingBudgetTokens is optional and independent — it does not replace the depth. Where a provider meters thinking in tokens, the budget is used and is capped to leave room for the answer; everywhere else the depth applies as usual. Setting one never voids the other.

Google and OpenAI support is per model, not per generation, and is read from hand-maintained tables measured against the live APIs.

On Google, gemini-3-pro-preview accepts only low and high where gemini-3.6-flash accepts all four, and gemini-2.5-pro cannot stop thinking where the flash models can. A Gemini model with no verified entry receives no thinking options at all and runs at its own default, because guessing the dialect is how a stock install starts returning 400s.

On OpenAI, most models take all three tiers, but the GPT-5 -pro models do not (o1-pro and o3-pro take all three): gpt-5-pro accepts only high — and rejects medium, the default depth, so it is the one to know about. gpt-5.2-pro, gpt-5.4-pro and gpt-5.5-pro reject low. An OpenAI model with no entry is assumed to take all three, the opposite of the Google default, because nearly every model does and the adapter already skips non-reasoning ones.

In both cases a requested depth a model doesn’t offer steps down to the nearest one it does, never up. Where nothing at or below the request is offered, no depth is sent and the model runs at its own default — so an explicitly set thinkingEffort can end up not applied on a model that doesn’t accept it, rather than the call failing.

off is not universally enforceable, and where it isn’t, nothing is sent rather than a flag that would be rejected or ignored:

  • Anthropic — the AI SDK only serializes thinking for enabled and adaptive, so a disabled flag never reaches the wire for any model (#738). off works where it works because Anthropic’s own default is not to think.
  • OpenAIreasoningEffort: "none" is documented as GPT-5.1-only and an error elsewhere. On o-series models reasoning can’t be turned off at all.
  • Gemini — 2.5 Flash takes a zero budget; 2.5 Pro cannot disable thinking. On Gemini 3 it depends on the model: those offering a minimal level get it, and those that don’t (both Pro entries) get nothing rather than the next tier up, which would answer “don’t reason” with an instruction to reason.

The models object declares two role-based slots. Each value is a provider:model-id string (e.g. "anthropic:claude-sonnet-4-6").

{
"models": {
"default": "anthropic:claude-sonnet-4-6",
"fast": "anthropic:claude-haiku-4-5-20251001"
}
}
SlotUsed for
defaultChat, general requests, most tool-using turns
fastConversation titles, the home briefing, and both history folds — end-of-turn compaction and the mid-turn fold. Cheap/fast workloads, but still needs a large context window: see below

A workspace can override individual slots via workspace.json → models. When a nb__delegate call or agent profile specifies a slot name (e.g. "model": "fast"), it’s resolved against these slots. "alias:fast" is the same reference written explicitly; both spellings resolve identically.

A bare model string that is neither a slot name nor a model in the catalog is passed through to the provider unchanged, with anthropic: prepended — the deliberate fallback for pinned or bespoke ids served under their own name (for example through an OpenAI-compatible proxy). That means a typo here is not rejected at config load; it fails at the provider on the first call that uses the slot, reporting a model you did not configure.

Changing a slot affects new conversations only

Section titled “Changing a slot affects new conversations only”

A conversation is bound to a model when it is created, and that binding does not move. Changing models.default — in this file, through the override file, or via the admin set_model_config tool — applies to conversations started after the change; conversations already under way keep running on the model they began on. Start a new conversation to pick up the new slot.

The binding is the main chat model only. Auxiliary work always follows the current slots: title generation and history compaction take fast, and a nb__delegate call takes whatever slot it names.

Precedence for a chat turn, highest first:

Source
1The conversation’s binding, set when it was created
2A per-request model, on the turn that creates the conversation
3models.default from this file (or the override file)

Conversations created before this behavior existed carry no binding and resolve from the current slot on every turn, as they did before.

Which models this organization permits. Absent — or present with an empty allowed — allows every chat-capable model from every configured provider, so an existing deployment changes nothing and an org opts into restriction.

{
"modelPolicy": {
"allowed": ["anthropic:claude-sonnet-5", "anthropic:claude-haiku-4-5-20251001"]
}
}
FieldTypeDescription
allowedstring[]Qualified provider:model-id strings.

Policy can only subtract. It narrows what the configured providers can already reach — naming a model whose provider has no key does not make it reachable, and naming a non-chat model does not make it selectable.

This file is the only place it is set. There is no runtime surface for it — the allowlist is deployment configuration, so it lives in version control with the rest of the tenant’s setup and a redeploy always restores it.

Every slot in models should name an allowed model. Nothing stops you writing a file where one does not: the runtime logs an error naming the slot at startup and then runs, because a deployment that boots with an error you can act on beats one that will not start. Until you fix it, turns resolve to that model while the picker excludes it.

Once the file is loaded the runtime does hold the line — repointing a slot at a forbidden model, or clearing one so it falls back to a forbidden model, is refused. A user’s saved model preference needs no such care: one outside the list falls back to the configured default on their next turn.

Per-provider API keys. Takes precedence over the legacy model object when both are present.

{
"providers": {
"anthropic": { "apiKey": "sk-ant-..." },
"openai": { "apiKey": "sk-..." },
"google": { "apiKey": "AIza..." }
}
}

Each provider key is optional. If a provider’s apiKey is omitted, the runtime falls back to that provider’s standard env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, NEBIUS_API_KEY, XAI_API_KEY).

nebius is an OpenAI-compatible gateway for open-weight models (Qwen, Kimi, GLM, gpt-oss). Configure it like any other provider and reference models with the nebius: prefix:

{
"providers": {
"anthropic": { "apiKey": "sk-ant-..." },
"nebius": { "apiKey": "..." }
},
"models": {
"default": "anthropic:claude-sonnet-4-6",
"fast": "nebius:openai/gpt-oss-120b"
}
}

The key falls back to NEBIUS_API_KEY. Unlike the other providers, this key is required when nebius is configured — the runtime fails to start if neither providers.nebius.apiKey nor NEBIUS_API_KEY is set, because the Nebius adapter reads no environment variable of its own and a keyless provider has nothing to authenticate with. Override the endpoint with baseURL (default https://api.tokenfactory.nebius.com/v1). A curated set of flagship models ships in the catalog so they appear in the model picker with pricing; any other Nebius model id still routes as free text (nebius:<org>/<model>), just without catalog metadata. Nebius requests use the Chat Completions API and rely on the provider’s automatic prefix caching — the Anthropic-style explicit cache breakpoints don’t apply.

Reasoning models hosted here stream their thinking back and it is shown in the chat. Those blocks carry no provider signature, so a Nebius turn that hits the output limit mid-answer stops there and reports why, instead of automatically continuing the way an Anthropic turn does.

The exact model lineup is account- and tier-specific and changes over time. The authoritative list for your key is GET https://api.tokenfactory.nebius.com/v1/models; use those ids verbatim (they’re case-sensitive, e.g. Qwen/Qwen3-235B-A22B-Instruct-2507).

xai reaches Grok over the Chat Completions API. Configure it like any other provider and reference models with the xai: prefix:

{
"providers": {
"anthropic": { "apiKey": "sk-ant-..." },
"xai": { "apiKey": "xai-..." }
},
"models": {
"default": "xai:grok-4.5"
}
}

The key falls back to XAI_API_KEY, and baseURL overrides the endpoint (default https://api.x.ai/v1). Five models ship in the catalog: grok-4.5, grok-4.3, grok-4.20-0309-reasoning, grok-4.20-0309-non-reasoning, and grok-build-0.1. All five support tool calling.

Reasoning support is uneven in a way worth knowing before you point a slot at one:

ModelEffort tiers accepted
grok-4.3low, medium, high, plus none to suppress reasoning entirely
grok-4.5low, medium, high
grok-4.20-0309-reasoningnone — it reasons, but rejects the effort parameter
grok-build-0.1none — same
grok-4.20-0309-non-reasoningnone — does not reason

Where a model takes no effort parameter, the runtime sends no reasoning options and the model applies its own default; asking for a tier would fail the call outright. grok-4.3 is the only model in an effort dialect where thinking: "off" sends a suppressing value rather than nothing — none genuinely zeroes the reasoning trace. (Gemini 2.5 models that can disable thinking also honor off, via a zero thinkingBudget; that is the budget dialect, not a tier.)

Two other notes. xAI publishes no per-model max-output cap and accepts max_tokens up to the full context window, so grok-4.5 and grok-build-0.1 are pinned to a 16,384-token output ceiling in the catalog — without it the message budget resolves to zero and every turn fails. And grok-4.20-multi-agent-0309 is deliberately absent: the Chat Completions endpoint refuses it (Multi Agent requests are not allowed on chat completions).

Conversations are workspace-owned and persisted automatically — there is nothing to configure. Every turn is written to a per-conversation JSONL file at {workDir}/workspaces/<wsId>/conversations/<ownerId>/<convId>.jsonl in the event-sourced format (one append-only event log per conversation), which is what enables crash-resilient run replay and history compaction. Legacy message-format JSONL files are still read.

Pluggable metadata store for MCP HTTP sessions. Tracks each Mcp-Session-Id and its idle TTL across the cluster.

{
"sessionStore": {
"type": "memory",
"ttlSeconds": 28800
}
}

Process-local Map with periodic TTL sweep. No external dependencies. Default; fine for any single-replica deploy.

FieldTypeDefaultDescription
type"memory" | "redis""memory"Provider. redis requires redis.url.
ttlSecondsinteger (s)28800 (8 h)Idle TTL. Each request resets the clock — actively-used sessions never expire.
redis.urlstringRedis connection URL (redis:// or rediss://). Required when type is "redis". Supports ${VAR} env expansion.
redis.keyPrefixstring"nb:mcp:session:"Hash key prefix for session entries.

The MCP /mcp endpoint is stateful. Each session has two parts:

  • MetadatasessionId, identityId, workspaceId, timestamps. Just data; lives in the session store.
  • Live transport — open HTTP response stream, SDK Server instance, registered request handlers, in-flight JSON-RPC state. Process-bound — holds open file descriptors and JS object references that cannot be serialized or moved.

Metadata is shareable across processes; the live transport is not. Routing requests to the process that owns a session’s transport is the load balancer’s job (cookie stickiness, header-hash on Mcp-Session-Id), not the session store’s. The session store is deliberately deployment-vocabulary-free — no notion of “which pod owns this” — so the same interface fits any deployment topology.

When a request arrives at a process that doesn’t have the live transport for the requested session, the 404 carries error.data.reason:

ReasonMeaning
not_foundSession store has no record. Idle-TTL eviction, or session never existed.
unavailableSession exists in the store, but the live transport isn’t on this process. Possible causes: process restart, sticky-routing miss, transport closed locally.

Spec-compliant clients re-initialize on either reason. Operators distinguish unavailable causes via deploy timing, process uptime, and “session-store size vs local transport count” signals.

ReplicasStoreUse case
1memoryDefault. Dev, programmatic use, simple production. Sessions die on process restart; clients re-initialize.
1redisSingle-process with cluster-shared metadata for ops visibility, audit trails, or future scale-out.
NredisHorizontal scale. Pair with sticky load-balancing (e.g. ALB lb_cookie) — without it, every cross-process request returns unavailable and clients re-initialize constantly.
NmemoryNot supported. Sessions partition across processes.

When type: "redis" is set without a redis.url, NimbleBrain refuses to fall back to in-memory and fails the boot — silently degrading would mask a misconfiguration.

platform.replicas > 1 requires all of:

  1. Storage that supports concurrent multi-process mounts. The default chart’s PVC is ReadWriteOnce and blocks RollingUpdate rollouts. Move workspace data out of the PVC (Postgres/S3) or relocate to ReadWriteMany (EFS).
  2. Routing strategy keyed on Mcp-Session-Id. ALB lb_cookie stickiness is the simplest path on AWS; alternatives are NGINX/Envoy with header-hash routing, or application-level cross-process proxying.
  3. Cluster-shared session store. sessionStore.type: "redis".
  4. Deploy strategy that doesn’t drop all replicas at once. platform.strategy.type: RollingUpdate (with the storage from (1) in place).

Drop any one of these and multi-replica is broken in a different way: PVC deadlock, re-init storms, partitioned session view, or deploy-time outages.

The 8 h default is tuned for a connector left open during a working day. Tighten it (e.g. 1800 for 30 minutes) if you want sessions to evict faster on idle; loosen it if your typical usage pattern leaves connectors open across days.

MCP_SESSION_TTL_SECONDS env var overrides this when set.

{
"logging": {
"dir": "~/.nimblebrain/logs",
"disabled": false,
"level": "normal",
"retentionDays": 30
}
}
FieldTypeDefaultDescription
dirstring{workDir}/logsLog directory.
disabledbooleanfalseDisable structured logging entirely.
level"normal" | "debug""normal""debug" persists additional verbose fields.
retentionDaysintegerAuto-delete log files older than N days on startup. Omit for no cleanup.

Managed-connector providers. See Connector Providers for what one is, how it differs from a connector, and which providers ship.

A provider is registered — and its callback routes, liveness probe, and vendor SDK reached — only when everything it needs to identify itself resolves: the broker credential for both, plus a namespace for Smithery (see its table below). A deployment without one carries no provider: the /v1/composio-auth/* routes are not mounted, no revalidation sweep runs, and the vendor SDK is never loaded.

{
"connectors": {
"providers": {
"composio": {
"apiKey": "",
"authConfigs": { "gmail": "ac_xxxxxxxx" },
"baseUrl": "https://backend.composio.dev",
"monitorEnabled": true
},
"smithery": {
"namespace": "my-namespace",
"monitorEnabled": true
}
}
}
}
FieldTypeDefaultDescription
apiKeystringPlatform-wide Composio API key — the broker credential. See The broker credential.
authConfigsRecord<string, string>{}Per-toolkit auth-config ids (ac_…), keyed by the toolkit slug the catalog entry names. See Per-toolkit auth configs.
baseUrlstringhttps://backend.composio.devComposio API base URL override (self-hosted / staging). Must be http(s) — any other scheme is rejected at startup, because it would turn /v1/composio-auth/proxy into an open redirect.
monitorEnabledbooleantrueRun Composio’s arm of the connection-revalidator probe, which detects connections whose downstream vendor account lapsed while the platform key stayed valid.

The probe’s sweep cadence is not here. One revalidator serves every provider, so the interval is provider-agnostic and is set with NB_CONNECTION_REVALIDATE_INTERVAL_SECONDS (Environment Variables). Only the enable/disable is Composio’s.

Every auth: composio connector needs an auth config created in the Composio dashboard (how). Its ac_… id varies per Composio account, so it cannot live in the catalog, which is shared across deployments.

"authConfigs": { "gmail": "ac_xxxxxxxx", "posthog": "ac_yyyyyyyy" }

The key is the catalog entry’s composio.toolkit — an identifier that entry already carries, rather than a third name invented to carry the value. It is still matched exactly, so a mistyped key resolves to nothing; the runtime checks the whole set at boot and logs any key matching no catalog toolkit.

A toolkit you leave out is not reported — wiring a subset of the catalog is the normal case, so a standing boot warning listing the rest would only teach operators to ignore [composio]. Installing a connector whose toolkit has no id fails at that moment, naming the toolkit and the config key to set.

These are identifiers, not secrets: an ac_… is inert without apiKey, the way a client_id is inert without a client_secret. They live in config next to the toolkits they name rather than in your secret store.

One instance serves one tenant, so this is a single config layer: there is no per-tenant merge. Intra-tenant isolation is unchanged — the single broker account still serves every workspace and user in the tenant, namespaced by the Composio user_id.

FieldTypeDefaultDescription
apiKeystringPlatform-wide Smithery API key. Falls back to SMITHERY_API_KEY. Its presence, with a namespace, is the gate on registering the provider at all.
namespacestringSmithery namespace brokered connections are created under. Required — a namespace is globally unique and account-owned at Smithery, so there is no safe default; without one the provider stays unregistered even with a credential present.
baseUrlstringhttps://api.smithery.aiConnect API base URL override. Must be http(s) — the session URL derived from it becomes an installed connector’s remote MCP target.
monitorEnabledbooleantrueRun Smithery’s arm of the connection-revalidator probe. It detects a connection deleted at the broker; a lapsed authorization is logged, not flipped, until a verdict can carry a remedy URL.

Smithery contributes no callback routes: it brokers OAuth on its own hosted setup page rather than redirecting back to us, so there is no equivalent of /v1/composio-auth/* to mount. See Connectors Catalog for authoring an auth: smithery entry.

apiKey is the platform-wide Composio API key and the gate on registering the provider at all — without it (here or in COMPOSIO_API_KEY) the integration stays dormant.

It is declarable because nothing persisted points at where it lives. A connector installed through Composio records auth: { type: "provider", provider: "composio" } in workspace.json — it names the credential it needs, not a place to find it — so the key never sits at rest and resolution stays the provider’s own business. Connectors installed before that change carry a ${COMPOSIO_API_KEY} environment reference and are mapped forward when they start, so both resolve identically with no operator action.

The broker credential is the only env fallback

Section titled “The broker credential is the only env fallback”

Settings resolve from this block only — there is no environment fallback for them. The broker credential is the exception: apiKey also reads COMPOSIO_API_KEY / SMITHERY_API_KEY, because a secret has a legitimate home in the environment. One value, one source, with that single documented exception.

NB_TENANT_ID is likewise not part of this block. It is a deployment identity, stays env-only, and is still required when the OAuth bouncer is configured.

File upload limits applied when features.fileContext is enabled.

{
"files": {
"maxFileSize": 26214400,
"maxTotalSize": 104857600,
"maxFilesPerMessage": 10,
"maxExtractedTextSize": 204800
}
}
FieldTypeDefaultDescription
maxFileSizeinteger (bytes)26 MBMaximum size of a single uploaded file.
maxTotalSizeinteger (bytes)100 MBMaximum total size of all files per message.
maxFilesPerMessageinteger10Maximum number of files per chat message.
maxExtractedTextSizeinteger (bytes)200 KBMaximum extracted text size per file.

NimbleBrain collects anonymized, aggregate usage data with no PII — no bundle names, tool names, error messages, or file paths are sent.

{ "telemetry": { "enabled": false } }

Disable with NB_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 env vars, or set telemetry.enabled: false as shown above.

The older single-provider model shape is still accepted for backward compatibility but deprecated. Prefer providers for mixing API keys and models for model selection.

{
"model": { "provider": "anthropic", "apiKey": "sk-ant-..." },
"defaultModel": "claude-sonnet-4-6"
}
FieldTypeDescription
provider"anthropic" | "openai" | "google"Default provider when providers isn’t set.
apiKeystringFalls back to the provider’s env var when omitted.

NimbleBrain validates nimblebrain.json at startup against the JSON Schema (draft-07) using AJV.

  • Unknown keys produce a warning on stderr and are ignored.
  • Structural errors (wrong types, missing required fields) throw an error and prevent startup.
  • Workspace-owned and legacy keys (bundles, skillDirs, agents, home, preferences, noDefaultBundles, and the legacy skills) are silently stripped — no warning.
  • The override file (nimblebrain.overrides.json) is validated structurally, but unknown-key warnings are suppressed for it. Every key set_model_config writes is in the published schema; the suppression is for version drift, since the file is preserved across deploys and can outlive the schema of whichever image reads it.
  • Deprecated keys (identity, contextFile) produce a warning pointing at context skills.
[config] Warning: unknown key "foo" in /home/user/.nimblebrain/nimblebrain.json (ignored)
Error: Invalid config in /home/user/.nimblebrain/nimblebrain.json:
- /maxIterations: must be <= 50

CLI flags take precedence over values in the config file:

FlagOverrides
--config <path>Config file location
--workdir <dir>Default location of the config file and workDir fallback
--model <id>defaultModel field (legacy)
--port <n>http.port field
--debugEnables verbose debug event logging

The smallest valid config file:

{
"$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json",
"version": "1"
}

This uses all defaults: Anthropic provider (key from ANTHROPIC_API_KEY), default model, no MCP bundles installed (platform capabilities are built in), in-memory conversation storage, and structured logging enabled.