Configuration
devbot.jsonc — project and global configuration.
devbot.jsonc
Configuration file for dev-bot agent projects. JSONC format — supports // comments and trailing commas.
Two files exist at different scopes, both optional:
Locations
| File | Scope | Purpose | Created by |
|---|---|---|---|
<project>/.devbot.project.jsonc |
Per-project | Project-specific settings | devbot init |
<devbot-root>/.devbot.global.jsonc |
Global | Shared defaults across all projects | make install |
Resolution
Scalar settings resolve project-over-global, falling back to a built-in default when neither is set (devbot_dir, harness). Map settings (modules) merge per key, the project value winning; list settings (devbot:guards) are concatenated and evaluated first-match-wins (project rules first, then global). search-memories reads only the project config. If a file is missing, its settings are skipped.
Auto-reinit on config change
Both config files are the source of truth for wiring — changing one (e.g. a modules flip, a provider key, gpu_enabled) only takes effect after a reinit. Rather than requiring a manual devbot reinit, the next bare devbot start detects the change and reinits the current project automatically, before up.sh runs, so the whole start sequence runs on freshly wired state.
Detection is a single per-project content hash over both configs, stored at <project>/.devbot.project.sha (the project config path with its .jsonc extension replaced — never committed). init/reinit clear the baseline before touching the wiring and restore it at the end of a successful run, so an unchanged wiring starts without re-running reinit, while an interrupted reinit leaves the baseline missing and the next start repairs the tree. A project with no .sha yet (freshly added, or upgraded from before this feature) triggers one reinit to establish it.
Because the baseline is per project, a global change (including the version bump devbot update writes) makes every project reinit on its own next start — there is no global baseline file.
- If reinit succeeds, the start proceeds normally.
- If reinit fails, dev-bot reports the error and aborts the start (non-zero exit) — the harness never launches on half-built wiring. Fix the cause and run
devbot reinit. (There is no “continue anyway” override.) - Editing
opencode.jsonc/.mcp.jsondirectly does not trigger this — those are outputs of init, not inputs. make up/devbot upalone do not trigger it — only a baredevbotstart (harness launch).up.shrefreshes the baseline only when it was already clean, so a standalonedevbot upcan never consume a pending change.
Auto-update on start
A bare devbot start runs devbot update first (quietly), so the install is brought to the newest release before wiring. update is a clean no-op when already on the newest tag. Set the global auto_update to false to opt out.
When update moves to a new release it writes the tag into the global config’s version. That changes each project’s wiring hash, so every project reinits on its own next start — see Auto-reinit on config change. If the update fails (offline, conflict), dev-bot warns and continues the start on the current version.
Properties
version
{ "version": "1.4.0" }
Type: string
Required: no
Default: absent (the dist config seeds "")
Scope: global
The installed dev-bot release tag. Written by devbot update when it moves onto a new release; not meant to be edited by hand. A change here changes each project’s combined wiring hash, so every project reinits on its next start.
auto_update
{ "auto_update": true }
Type: boolean
Required: no
Default: true
Scope: global
When not false, a bare devbot start runs devbot update --auto before wiring. The update is a no-op when already on the newest release; a failure warns and the start continues on the current version.
project_name
{ "project_name": "my-project" }
Type: string
Required: no
Default: "devbot" (fallback)
The project name. Used by search-memories and qmd to auto-detect the QMD collection name (falls back to "devbot" if absent).
harness
{ "harness": "opencode" }
Type: string ("opencode" | "claudecode")
Required: no
Default: "opencode"
Selects which harness DevBot configures. Project config takes precedence over global; invalid values fall back to "opencode".
Used by _devbot_get_harness to select the launch script (src/harnesses/<harness>/start.sh).
devbot_dir
{ "devbot_dir": ".agents" }
Type: string
Default: ".agents"
Required: no
Base directory for all devbot project state: memory vault, logs, thinking files. Set to override the default location (e.g. .my-custom-state). All devbot subdirectories (memory/, logs/, etc.) live under this path relative to the project root.
Used by memory init, qmd init, the auto-recover and remember-session plugins, harness delegation, module symlinking, and gitignore setup.
commit_memory
{ "commit_memory": false }
Type: boolean
Default: false
Required: no
When true, the memory vault is committed to version control instead of being gitignored. When false (default), the memory/ directory is excluded via .git/info/exclude.
Project config takes precedence over global: an explicit value in .devbot.project.jsonc wins; otherwise the value from .devbot.global.jsonc is used; unset in both means false.
Used by memory init and the devbot init gitignore step.
modules
{ "modules": { "claudecode": false, "litellm": false } }
Type: object (map of module name → boolean)
Default: {} (every module enabled)
Required: no
Scope: project overrides global per key
Per-module enablement. A module set to false is skipped during lifecycle scripts (init, install, update, prereq checks) — its scripts, symlink wiring, and declared MCP servers are all skipped. A module absent from both files is enabled. The global and project maps merge per key, with the project value winning, so a project can re-enable a globally disabled module, or vice versa.
Disabling a module also removes its MCP servers from every harness config — this is the lever for dropping a heavy server’s tool-schema and process cost (see MCP configuration).
codebase_index_provider
{ "codebase_index_provider": "codebase-index" }
Type: string ("codebase-index" | "codebase-memory")
Default: "codebase-memory"
Required: no
Scope: global only
Selects which codebase-understanding engine is active. The two engines are
interchangeable — the same slot in the agent toolkit, swapped by flipping this
one key (then running devbot reinit):
| Value | Engine | Integration |
|---|---|---|
"codebase-index" |
opencode-codebase-index (Ollama nomic-embed-text embeddings) |
opencode plugin + MCP |
"codebase-memory" |
codebase-memory-mcp (DeusData — bundled native embeddings, no Ollama) |
MCP server |
The two modules are mutually exclusive: _devbot_get_disabled_modules
auto-appends the non-selected engine to the disabled set, so exactly one is
ever wired. An explicit modules-map false on the selected engine still
hard-disables it (no codebase engine); enabling the non-selected one via
modules is ignored.
Upgrade note: existing installs that do not set this key get the
codebase-memory default and will stop registering codebase-index on the
next reinit. Pin "codebase_index_provider": "codebase-index" before
updating dev-bot to keep the previous engine.
Behaviour on disable (read before flipping): disabling a module — via this
key or the modules map — unregisters its declared opencode plugin/MCP
entries at the next devbot reinit, not merely stops future registration. This
is required for the engine swap: a flipped-off engine must shed its
registrations. devbot init/reinit rewrite opencode.jsonc, .mcp.json and
the per-project config from module manifests — existing configs are not
backed up; the old file is replaced. Skills, agents, commands, and tool
symlinks are removed outright and regenerated from source. Review the current
config before reinit if you have hand-tuned entries for a module you are about
to disable.
memory_search_provider
{ "memory_search_provider": "qmd" }
Type: string ("qmd" | "mdctx")
Default: "mdctx"
Required: no
Scope: global only
Selects which memory-search engine is active. The two engines are
interchangeable — the agent-facing entry point (search-memories) stays the
same and dispatches to whichever engine this key selects (both search the
project memory vault and the shared global store); the engine’s own MCP server
and skill are registered for the selected engine only. Swap by flipping this
one key, then running devbot reinit:
| Value | Engine | Integration |
|---|---|---|
"qmd" |
@tobilu/qmd (BM25 keyword search over per-project collections — no models, no embeddings) |
CLI (guard-blocked for agents) |
"mdctx" |
mdctx (zachkepe/mdctx — zero-ML-dependency RAKE + BM25 keyword index over a flat, git-diffable JSON file; no embeddings, no GPU) |
MCP server + CLI |
The two modules are mutually exclusive: _devbot_get_disabled_modules
auto-appends the non-selected engine to the disabled set, so exactly one is
ever wired. An explicit modules-map false on the selected engine still
hard-disables it (no memory-search engine); enabling the non-selected one via
modules is ignored. The qmd/mdctx pair is independent of the
codebase_index_provider pair — both auto-exclusions apply.
Both engines are keyword-only: mdctx is keyword-only by design (RAKE +
BM25, no embeddings); qmd is used BM25-only through the memory module — no
model download, no embeddings (ADR 20260913072905-qmd-bm25-only-no-model-
downloads). Memory search is deterministic BM25 against descriptive
titles/keywords under either engine.
Upgrade note: existing installs that do not set this key get the mdctx
default and will stop registering qmd on the next reinit. Pin
"memory_search_provider": "qmd" before updating dev-bot to keep the
semantic/GPU engine and its existing collections.
Flip: after changing the key, run devbot reinit so the newly selected
engine’s MCP/skill is registered, the losing engine’s entries are pruned, and
its index is built (mdctx build of the project vault + global store, or qmd
collection registration).
Behaviour on disable matches codebase_index_provider above: disabling a
module unregisters its declared plugin/MCP entries at the next devbot reinit.
devbot:guards
{
"guards": [
{ "regex": "rm -rf", "message": "rm -rf is blocked" },
{ "regex": "sudo .*", "message": "sudo is blocked" },
{ "regex": "git push --force", "message": "force push is prohibited" },
],
}
Type: array of guard objects
Required: no
Each guard rule has:
| Field | Type | Description |
|---|---|---|
regex |
string |
Regex pattern matched against the shell command (case-sensitive) — the bash tool, or a PTY invocation normalised to command args |
message |
string |
Block reason shown to the user when the rule matches |
Used by the guards module (on-tool_execute_before-guards.ts opencode hook, on_tool_execute_before-guards.sh claudecode hook). Guards from the global config and the project config are concatenated; the first matching rule wins (project rules are evaluated before global rules).
auto_recover.max_attempts
{
"auto_recover": {
"max_attempts": 5,
},
}
Type: number (inside auto_recover object)
Default: 5
Required: no
Maximum number of automatic recovery attempts per session after a transient provider error (API timeout, 5xx, overloaded). Prevents infinite retry loops — when the ceiling is hit, the error surfaces to the user.
Used by the auto-recover plugin (on-session_error-auto-recover.ts).
gpu_enabled
{ "gpu_enabled": false }
Type: boolean
Default: false
Required: no
Scope: global only
Enables GPU acceleration for local inference (QMD/Ollama). Set automatically by devbot install / devbot update (devbot-level detection, not the ollama module install); never by a bare start.
A module that needs GPU passthrough ships a docker-compose.gpu.yml beside its compose (ollama’s is src/tools/ollama/docker-compose.gpu.yml; codebase-index ships one that include:s it). It is appended only when this is true and a live container-passthrough probe (_has_docker_gpu) succeeds — on Docker Desktop (macOS/Windows) passthrough is unavailable, so a stray true cannot break devbot up. Because the overlay follows the compose it overrides, it can never be applied without the service it targets. qmd’s own GPU selection is independent of this flag: it probes the host directly.
ollama_local_api
{ "ollama_local_api": "http://localhost:18434" }
Type: string
Default: "http://localhost:18434"
Required: no
Scope: global only
Local Ollama API endpoint, read by the ollama model-pull helper (_ensure_ollama_models_detached in src/_shared/functions.sh) when it fetches missing models; the OLLAMA_API_URL environment variable overrides it for a single run. It is dormant unless the codebase-index engine is enabled.
projects
{ "projects": ["/path/to/project-a", "/path/to/project-b"] }
Type: array of string
Default: []
Required: no
Scope: global only
Absolute paths of projects registered with DevBot. Managed by the project-registration helper (add_project.py).
datasources
// .devbot.global.jsonc — the catalogue, declared once
"datasources": {
"hotels-dev": {
"type": "mysql",
"env": {
"MYSQL_HOST": "localhost",
"MYSQL_USER": "root",
"MYSQL_PASSWORD": "${HOTELS_DEV_DB_PASSWORD}"
}
}
}
// .devbot.project.jsonc — opt in, by name
"datasources": ["hotels-dev"]
Type: object (global) / array of names (project)
Default: {} / []
Required: no
Scope: the catalogue is global-only; the selection is per project
Database access for agents, through one shared MCP Toolbox gateway on
127.0.0.1:18510. The catalogue is declared once; each project opts in by name.
A datasource a project does not name is never registered with its harness, so it
costs nothing — not even context.
The env map names the engine’s own variables (MYSQL_HOST, MONGODB_URI,
MONGODB_DATABASE, …). Each value is either a literal or a ${VAR}
reference to an environment variable:
| Value | Meaning |
|---|---|
"localhost" |
A literal, written into the rendered config |
"${HOTELS_DEV_DB_PASSWORD}" |
Resolved from the environment — the value reaches no file |
A reference must be the whole value: pre-${VAR} is a literal, so a password
containing ${ is never mistaken for one. Non-secret values read best inline,
and anything secret belongs behind a reference. The environment is the repo
.env (which devbot up loads) or your shell.
Fields left out fall back to the engine’s defaults, so declare only what
differs: MYSQL_PORT defaults to 3306, and an omitted MYSQL_DATABASE leaves
the source with no default schema — one datasource then covers every database
on the instance, reachable by qualifying names
(SELECT ... FROM otherdb.sometable). MongoDB is the exception: its aggregate
tool requires a database, so one mongo datasource covers one database. A
mongodb+srv:// URI is resolved by the toolbox itself — dev-bot never parses
the URI, so SRV records need no port assumption.
type is mysql (MariaDB included), postgres, sqlite, mongodb or
redis — or a sidecar type (opensearch, s3), served by its own container
instead of by the shared gateway (see Sidecar datasources).
Redis fits the same one-free-form-tool shape, by a different route. Its
toolbox tool runs a fixed command list — but an array argument is flattened
into the command, so a single array parameter templating the whole command makes
the command name a runtime value too: the agent supplies
["GET", "some-key"], exactly as it supplies a SQL statement elsewhere. One
endpoint per datasource (address is a sequence upstream, even for a single
host), and username / password are omitted unless declared — an empty AUTH
string is not the same as no AUTH.
Two behaviours are worth knowing before relying on it:
- Only the databases reachable at startup are loaded. dev-bot opens no
database connections of its own: it runs the real toolbox (the only DB client)
against the declared sources and loads only the ones it accepts — so a down
database, a missing credential or a blocked host is excluded rather than
taking the whole gateway down with it. The set is decided once, when
devbot upruns: a database that comes up later is not picked up until the nextdevbot up. Reasons are printed ondevbot up. - A connection that hangs is bounded. mysql and postgres carry a 2s connect
timeout, so a firewalled host fails fast instead of stalling the gateway’s
startup. MongoDB gets the same bound in its URI (
connectTimeoutMS/serverSelectionTimeoutMS) — set one yourself in the URI, or in the variable behind it, to override it for a cluster that honestly needs longer. The redis source exposes no dial timeout at all in toolbox 1.11.0, so a blackholed redis host is still excluded, but only after the canary’s full 5s deadline. - Nothing blocks writes. A datasource is exactly as writable as the database user it is given — which is how one config serves a writable dev database and a read-only production one. Point production at a read-only user.
Sidecar datasources
Some data sources have no toolbox source at all, so their type names a
sidecar: a small MCP server in its own container, on its own port, rather
than a toolset on the shared gateway. The declaration is unchanged — a type
and an env map — so a project opts in exactly as it does for a database.
"datasources": {
"prod-search": {
"type": "opensearch",
"env": {
"OPENSEARCH_URL": "https://search.example.com",
"AWS_REGION": "eu-central-1",
"AWS_ACCESS_KEY_ID": "${OPENSEARCH_KEY_ID}",
"AWS_SECRET_ACCESS_KEY": "${OPENSEARCH_SECRET}"
}
},
"artifacts": {
"type": "s3",
"env": {
"AWS_REGION": "eu-central-1",
"AWS_ACCESS_KEY_ID": "${ARTIFACTS_KEY_ID}",
"AWS_SECRET_ACCESS_KEY": "${ARTIFACTS_SECRET}"
}
}
}
| Type | Server |
|---|---|
opensearch |
the OpenSearch project’s opensearch-mcp-server-py, streamable-http |
s3 |
dev-bot’s own read-only S3 server — list_buckets, list_objects, head_object, get_object |
Ports come from the same block as the gateway (18500–18599, after 18510),
allocated by sorted datasource name, so a reinit renders the same ports
whatever order the catalogue is in.
Credentials are environment variables, like every other datasource: the
variable names are declared and compose interpolates the values from the
environment devbot up builds — no credential value is written to a config or to
a rendered file. Nothing mounts ~/.aws into a sidecar, so a sidecar that talks
to AWS takes AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN
(plus AWS_REGION) rather than a profile.
An unset ${VAR} a sidecar declares is reported as a WARN during the render —
not refused, because a sidecar with unset credentials still starts and fails only
when first called. Engines, by contrast, are excluded outright when their env is
incomplete.
A sidecar is not probed at startup. The availability canary exists because toolbox treats one unreachable source as fatal — a single down database would take every datasource with it. A sidecar is its own container and cannot do that, so one that fails to start fails only its own manifest, which surfaces when the agent first calls it.
Example: full project config
{
// Project identity
"project_name": "my-api",
// Harness selection
"harness": "opencode",
// Devbot state directory
"devbot_dir": ".agents",
// Commit the memory vault to version control
"commit_memory": false,
// Data sources this project may use, by name (see `datasources` above)
"datasources": ["hotels-dev"],
// Per-module enablement — `false` skips a module during lifecycle scripts
"modules": { "claudecode": false, "react": false, "signoz": false, "svelte": false },
// Guard rules for bash commands
"guards": [
{ "regex": "rm -rf", "message": "rm -rf is blocked" },
{ "regex": "sudo .*", "message": "sudo is blocked" },
{ "regex": "git push --force", "message": "force push is prohibited" },
],
// Auto-recovery
"auto_recover": {
"max_attempts": 5,
},
}
See also
.devbot.global.dist.jsonc/.devbot.project.dist.jsonc— shipped config templatessrc/tools/devbot-cli/init.sh— Writes the default project configsrc/agentic/guards/— Guard rule evaluationsrc/agentic/auto-recover/— Auto-recovery plugin