Configuration
August 22, 2026 · View on GitHub
The packaged npx bb-app flow stores persistent package settings under
~/.bb/config.json, provider environment values under ~/.bb/env.json, and
client SSH target mappings under ~/.bb/client.json.
Use bb-app config for non-secret bb settings:
npx bb-app config set BB_APP_URL https://<machine>.<tailnet>.ts.net
npx bb-app config set BB_INFERENCE codex/gpt-5.6-luna
npx bb-app config set BB_INFERENCE_FALLBACK codex/gpt-5.4-mini
npx bb-app config set BB_TRANSCRIPTION codex/gpt-transcribe
npx bb-app config list
npx bb-app config unset BB_APP_URL
npx bb-app config refresh
Use bb-app env for provider credentials and provider-specific environment:
npx bb-app env set OPENAI_API_KEY <key>
npx bb-app env list
npx bb-app env unset OPENAI_API_KEY
bb-app config list shows non-secret values. bb-app env list redacts every
value and only shows whether a key is set.
The Add machine installer may also store a machineCredential and its
connectMachineId beside serverUrl in config.json. The credential is a
secret managed by bb connect: do not copy, edit, or commit it. Both fields are
intentionally omitted from bb-app config list. At runtime they are passed to
the standalone host daemon and its bundled bb CLI as
BB_CONNECT_MACHINE_CREDENTIAL and BB_CONNECT_MACHINE_ID. These are
installer-managed transport details, not user configuration knobs; re-add the
machine instead of setting them by hand.
Use bb-app client ssh-target to let a local helper open files from a remote
bb server in local editors. The SSH target is the value that works after
ssh, such as devbox, user@devbox, or a Host entry from ~/.ssh/config:
npx bb-app client ssh-target set https://bb.example.test devbox --host-id host_abc
npx bb-app client ssh-target list
npx bb-app client ssh-target remove https://bb.example.test --host-id host_abc
Use --host-id when the server has more than one machine; copy the ID from
bb machine list. Omit it to preserve the single-machine auto-selection for
set, or to remove every mapping for that server with remove.
Precedence
Configuration is resolved in this order:
- Explicit launcher flags, such as
--data-dir,--server-port, or--server-bind-host. - Persistent
bb-app config,bb-app env, and client values. - Ambient shell environment.
- Built-in defaults.
For the packaged app, prefer bb-app config, bb-app env, and launcher flags
over shell variables. The environment remains the internal and deployment
substrate, and source-development commands still load .env files.
For source development, pnpm dev automatically injects
BB_DEV_CONNECT_BASE_URL=http://bb.localhost:<worktree-cloud-port>. The
Connect plugin accepts this loopback origin only when NODE_ENV=development
and uses it only as the unpaired default. Explicit bb connect --server ...
or --base-url ... targets take precedence, and packaged/production bb keeps
the https://getbb.app default. This value is launcher-managed, not a
bb-app config setting.
After bb-app config writes ~/.bb/config.json or bb-app env writes
~/.bb/env.json, it asks the running local server to reload. If bb is not
running, the new values apply on the next start. If you edit either file by
hand, run npx bb-app config refresh to apply the files to a running server.
The live reload applies config keys such as BB_APP_URL, BB_INFERENCE,
BB_INFERENCE_FALLBACK, and BB_TRANSCRIPTION, plus env values explicitly
consumed at runtime such as OPENAI_API_KEY. If one of those config keys is
stored with bb-app env instead, it is startup-only; use bb-app config when
you need a live change.
BB_LOG_LEVEL is the startup-only bb-app config key. The complete current
set of startup-only server or launcher env entries is:
BB_APP_SURFACE,BB_APP_URL,BB_DATA_DIR, andBB_DEV_APP_PORTBB_EXTERNAL_URL,BB_HOST_DAEMON_PORT,BB_INFERENCE,BB_INFERENCE_FALLBACK, andBB_INHERITED_SKILLS_ROOTSBB_LOG_LEVEL,BB_MANAGED_DEV_BUILTIN_PLUGIN_HOT_RELOAD,BB_MARKETPLACE_URL,BB_POSTHOG_API_KEY, andBB_TELEMETRYBB_SERVER_BIND_HOST,BB_SERVER_PORT,BB_TRANSCRIPTION, and allBB_FF_*feature flags
Setting or unsetting one still runs the reload for any other pending changes,
but the running processes keep their current values. Apply it with a full
launcher restart (bb-app stop && bb-app start) or by restarting the desktop
app. In particular, changing or unsetting BB_SERVER_BIND_HOST does not close
an existing 0.0.0.0 listener until that restart.
bb-app config refresh also notes any startup-only keys currently present in
config.json or env.json; those values apply on the next full restart.
When targeting a non-default running instance, pass the same --data-dir and
--server-port to bb-app config or bb-app env commands so they write the
right file and refresh the right server.
Stopping A Running bb
A running bb-app start writes <dataDir>/bb-app-runtime.json and removes the
file when it exits. The file records the launcher process id, the server URL,
the version, the start time, and how bb was started. Do not edit it.
Two things read that file:
npx bb-app stopstops the bb that owns the data directory. Pass the same--data-diryou started with when it is not the default~/.bb/.- The macOS desktop app asks before it uses a bb it did not start, and offers to stop that copy for you.
Both confirm that the recorded process really is a bb launcher before they signal it, so a stale file left by a crash cannot stop an unrelated process.
Common Keys
| Key | Command | When to set | Used for |
|---|---|---|---|
BB_APP_URL | bb-app config | Optional for remote use | Human-facing app URL used for generated links and allowed browser origins. Leave empty for local-only use. |
BB_INFERENCE | bb-app config | Optional | Primary server-side helper model in provider/model format. Defaults to codex/gpt-5.6-luna; the Codex helper route uses no reasoning. |
BB_INFERENCE_FALLBACK | bb-app config | Optional | Helper model used after a transient primary timeout, rate limit, or service-unavailable failure. Defaults to codex/gpt-5.4-mini. |
BB_TRANSCRIPTION | bb-app config | Optional | Voice transcription model in provider/model format. Defaults to codex/gpt-transcribe. |
BB_MARKETPLACE_URL | bb-app env, or environment | Startup-only testing | Manifest URL of the reserved bb-community plugin marketplace, which lists as BB Community. Defaults to https://getbb.app/marketplace/v1/marketplace.json; point it at a local file server to test catalog refreshes. It sets only the reserved bb-community marketplace; other marketplaces are added at runtime with bb marketplace add. A full launcher or desktop app restart is required. |
BB_SERVER_URL | bb-app config | Remote CLI/host use | Server URL for standalone bb CLI and host-daemon commands on the current machine. The CLI defaults to http://127.0.0.1:38886 when unset. |
BB_SERVER_BIND_HOST | bb-app env, environment, or --server-bind-host | Startup-only | Server listener host. Defaults to 127.0.0.1; accepts only 127.0.0.1 or 0.0.0.0. A full launcher or desktop app restart is required; until then, a previous 0.0.0.0 listener remains exposed. This is not a bb-app config key. |
BB_SERVER_PORT | bb-app env, environment, or --server-port | Startup-only | HTTP listener port. Defaults to 38886. A full launcher or desktop app restart is required after a persistent set or unset. |
BB_HOST_DAEMON_PORT | bb-app env, environment, or --host-daemon-port | Startup-only | Local host-daemon API port. Defaults to 38887. A full launcher or desktop app restart is required after a persistent set or unset. |
BB_LOG_LEVEL | bb-app config | Startup-only debugging | Log level: trace, debug, info, warn, error, or fatal. A full launcher or desktop app restart is required. |
OPENAI_API_KEY | bb-app env | OpenAI opt-in routes | Required only when selecting explicit OpenAI provider routes such as openai/gpt-4o-mini or openai/gpt-transcribe. |
By default, helper inference and voice transcription use Codex credentials from
the host daemon. Run codex login on the host for the default path. Set
provider env keys only when opting into a non-Codex provider route.
With a ChatGPT subscription login, codex/ voice transcription posts to a
chatgpt.com endpoint that sits behind Cloudflare bot protection. On some
networks Cloudflare challenges that request; bb retries, then reports
"Voice transcription is temporarily unavailable" and logs the Cloudflare
challenge on the server. If that happens often, route transcription through an
API key instead: codex login --with-api-key, or set BB_TRANSCRIPTION to
openai/gpt-transcribe with OPENAI_API_KEY.
The microphone picker in Settings → Voice Input is client-local. It stores the
selected browser MediaDevices device id in localStorage as
bb.voiceInput.audioInputDeviceId; it does not change bb-app config or the
server-side transcription model.
The builtin Keep Awake plugin has one autosaving configuration page with an
enable switch and an all-or-selected host picker. On selected macOS hosts it
runs /usr/bin/caffeinate -i -w <worker-pid> while enabled, preventing system
idle sleep while bb is running. It only blocks idle sleep: closing a laptop lid
or choosing Sleep manually still sleeps the Mac. Configure it from an agent or
terminal with:
bb keep-awake status [--json]
bb keep-awake enable [--json]
bb keep-awake disable [--json]
bb keep-awake hosts all
bb keep-awake hosts <host-id>...
The "Show unhandled provider events" toggle in Settings → General exposes raw
provider events that bb does not yet understand. It defaults to off in packaged
builds because these diagnostic payloads are noisy. Development builds continue
to show them regardless of the toggle. Set the persisted preference from an
agent or terminal with
bb settings general showUnhandledProviderEvents <true|false>.
The "Steer running threads on Enter" toggle in Settings → General changes the
active-thread composer shortcuts when no typeahead suggestion is active. It
defaults to off: Enter queues and Command+Enter steers. When enabled, Enter
steers and Command+Enter queues. Set it with
bb settings general steerActiveThreadOnEnter <true|false>.
The "Streamer mode" toggle in Settings → General hides every customModels
entry from ~/.bb/config.json in all model lists: the web and mobile pickers,
bb provider models, and sdk.providers.models. Turn it on before a screen
share so a private or early-access model id does not appear. It defaults to
off. The entries stay in config.json, and a thread that names a hidden model
explicitly still runs with it. Default model resolution for a new thread also
keeps the full list, so a provider whose only models are custom still starts.
A composer whose stored selection is a hidden model treats it as unavailable
and falls back to the provider default; the next send records that default, so
select the custom model again after you turn streamer mode off. Set it with
bb settings general streamerMode <true|false>.
Settings → Providers lists every registered agent provider in picker order.
Move a provider up or down to change the order and choose the default for new
threads. Both are persisted preferences: providerOrder is the list of ids
that lead the picker (ids not listed follow in plugin install order, and an id
that names no registered provider is ignored) and defaultProviderId is the
provider new threads use when neither the caller nor the project chose one
(null means the first available provider in picker order). Set them with
bb settings general providerOrder '["claude-code","codex"]' and
bb settings general defaultProviderId claude-code (or null).
Each provider's own options live on its plugin: Codex memory and native
subagents under the Codex provider plugin, Claude Code memory, native
subagents and the Workflow tool under the Claude Code provider plugin. Read
and set them like any plugin setting, for example
bb plugin config provider-claude-code set workflowsDisabled true.
Outside an open typeahead menu, Shift+Enter inserts a newline. On coarse-pointer touch devices, the software-keyboard Return path inserts a newline and the submit button sends. iPadOS WebKit additionally preserves the Enter and Command+Enter shortcuts above for a connected Magic Keyboard.
Keyboard Shortcuts
Mod+Shift+P opens the quick palette: type to filter, then run a command with
Enter. It lists only commands that apply on the current surface, shows each
one's shortcut, and offers recently run commands first. The numbered
accelerator families and the relative cycle commands stay rebindable but
unlisted. Plugins can add their own rows, listed under "Plugins".
Settings → Keyboard edits app command shortcuts. Overrides are stored in the server database, applied live to every connected window, and kept across restarts. Resetting a shortcut removes its override so future bb releases can continue to update the default. Clearing a shortcut explicitly disables that command. Command context and native-only availability remain server-owned and are not editable. Actions supported by both clients use the same resolved bindings in the browser and desktop app; browsers may still reserve some chords before bb receives them.
Mod means Command on macOS and Control on Windows/Linux. Numbered thread and
pane shortcuts follow Slack's browser-safe convention: web uses
Control+1…9 on macOS and Ctrl+Shift+1…9 on Windows/Linux, while desktop
uses Mod+1…9. The web aliases leave native browser Mod+1…9 tab switching
untouched. Previous and next thread use Mod+Shift+[/] on desktop and
Control+Shift+[/] on the web.
The "Show keyboard hints when holding CMD / Control" preference defaults
to on. Set it with
bb settings keyboard hints <true|false>. Turning it off hides the
delayed shortcut badges without disabling any shortcuts.
| Area | Command | Default | Availability |
|---|---|---|---|
| Palette | Quick palette | Mod+Shift+P | All clients |
| Threads | New thread | Mod+N / Mod+Shift+O | Desktop / web |
| Threads | Search threads | Mod+K | All clients |
| Threads | Rename focused thread | Unassigned | Thread view |
| Threads | Archive focused thread | Unassigned | Thread view |
| Threads | Previous / next thread | Surface defaults above | Desktop / web |
| Threads | Open visible thread 1–9 | Platform defaults above | Web / desktop |
| Layout | Previous / next chat pane | Unassigned | While split |
| Layout | Focus chat pane 1–8 | Platform defaults above | Split (web / desktop) |
| Layout | Maximize / restore chat pane | Mod+Shift+E | While split |
| Layout | Close focused chat pane | Mod+Shift+X | While split |
| Window | New window | Mod+Shift+N | Desktop |
| Window | Settings | Mod+, | All clients |
| Layout | Toggle sidebar | Mod+\ | All clients |
| Panel | New tab / close tab / toggle | Mod+T / Mod+W / Mod+J | All clients |
| Workspace | Quick open file / toggle diff | Mod+P / Mod+D | All clients |
| Workspace | Open terminal | Mod+Shift+Enter / Mod+Shift+T | Web / desktop |
| Workspace | Open in preferred app | Mod+O | All clients |
| Composer | Focus composer | Mod+Shift+C | All clients |
| Composer | Toggle model picker | Mod+Shift+M | All clients |
| Composer | Cycle model forward / backward | Alt+M / Alt+Shift+M | All clients |
| Composer | Cycle provider forward / backward | Alt+P / Alt+Shift+P | All clients |
| Composer | Cycle reasoning effort forward / backward | Alt+T / Alt+Shift+T | All clients |
| Browser | Focus location / reload / find in page | Mod+L / Mod+R / Mod+F | Desktop embedded browser |
| Questions | Choose visible answer 1–9 | 1 … 9 | While a question is open |
Cycle commands wrap in both directions. Reasoning cycles only through the current model's supported efforts in canonical low-to-high rank order, not the provider response order. The cycle shortcuts act only from the active composer or an open picker; unrelated editable controls retain their Option-composed character input. A configured app shortcut takes precedence in editable controls; when no matching command handles a chord, the control retains its native behavior.
The desktop application menu uses the same resolved bindings for New Thread, New Window, New Tab, Close, and Settings. There is no separate menu shortcut configuration.
BB_SERVER_URL does not change where full npx bb-app startup binds locally.
It is for commands that need to target an already-running server, such as the
bundled bb CLI or a standalone host daemon. The CLI can omit it when targeting
the default local packaged server at http://127.0.0.1:38886; set it for remote
or non-default servers.
Client SSH Targets
~/.bb/client.json is local to the machine showing the UI. The CLI resolves the
remote server's host ID and stores a mapping from that server/work-host to an SSH
target known to the local machine. The remote server does not read this file.
Example:
{
"servers": {
"https://bb.example.test": {
"hosts": {
"host_abc": {
"sshAuthority": "devbox"
}
}
}
}
}
When a remote bb page asks the local helper to open a work-host path, the helper uses this mapping to launch remote-capable editors and terminals over SSH. Browsers or devices without a helper can still use bb; local editor actions are simply unavailable.
Custom ACP Agents
Known ACP agents can appear automatically when their CLI is installed on the
host. For example, bb exposes acp-opencode when opencode is on PATH and can
be launched as opencode acp, and acp-omp when omp (oh-my-pi) is on PATH
and can be launched as omp acp. It also exposes acp-grok when Grok Build's
grok CLI is on PATH and can be launched as grok agent stdio, and
acp-hermes-agent when Hermes' hermes CLI is on PATH and can be launched as
hermes acp.
Register custom ACP agents by editing customAcpAgents in ~/.bb/config.json.
There is no bb-app config set or unset command for this list, matching the
manual-file workflow used for custom models. After editing the file, run
npx bb-app config refresh to apply it to a running local server, or restart bb.
Use customAcpAgents for arbitrary ACP agents, or to override the launch
command for a known provider id such as acp-opencode. To override
acp-opencode, set "id": "opencode"; bb derives the provider id by adding
the acp- prefix.
Example:
{
"customAcpAgents": [
{
"id": "my-agent",
"displayName": "My Agent",
"command": "my-agent",
"logo": "agent-logos/my-agent.svg",
"args": ["acp"],
"env": {
"MY_AGENT_MODE": "bb"
},
"cwd": "/Users/me/project",
"modelCli": {
"listArgs": ["--list-models"],
"selectFlag": "--model",
"primaryModels": ["default"]
},
"reasoningCli": {
"flag": "--reasoning-effort",
"supportedLevels": ["low", "medium", "high"],
"levelValues": {
"max": "high"
},
"defaultLevel": "high"
},
"nativeReasoning": {
"configId": "reasoning_effort",
"supportedLevels": ["none", "low", "medium", "high", "xhigh", "max"],
"defaultLevel": "medium"
}
}
]
}
id is a slug matching ^[a-z0-9][a-z0-9-]*$. bb derives the provider id by
prefixing it with acp-, so the example appears as acp-my-agent in
bb provider list, bb provider models acp-my-agent, and provider pickers.
The derived id must not collide with an always-visible built-in provider such
as acp-cursor or with another custom ACP agent. It may match an
installed-only ACP plugin provider such as acp-opencode, in which case the
custom config wins.
command is the executable name or path. bb runs it directly with the args
array; it is not a shell command line. env adds environment variables for the
agent process. cwd is optional; omit it to use the thread workspace directory.
logo is optional and accepts an SVG, PNG, or WebP file path. Relative paths
resolve from the bb data directory (for example,
~/.bb/agent-logos/my-agent.svg); absolute paths are also supported. bb serves
the file to app clients and uses it in provider and model pickers. Omit logo
to use a vendored brand icon for a recognized ACP id or the generic ACP icon.
modelCli is optional. When present, listArgs are used to ask the agent for
models, selectFlag is the flag bb passes when launching with a selected model,
and primaryModels marks preferred models in the picker. ACP agents that
advertise models over the protocol are auto-discovered without modelCli; keep
modelCli for CLI-style agents such as Cursor.
reasoningCli is optional. Use it only when the agent accepts reasoning as a
global launch flag rather than advertising a protocol thought_level option or
encoding effort in model ids. flag is inserted before the ACP agent args,
supportedLevels controls the picker levels, defaultLevel controls the
picker default, and levelValues maps bb reasoning levels to the agent's CLI
vocabulary when they differ.
supportsManualCompaction is optional and defaults to false. Set it to
true only when the agent accepts an explicit compaction request; ACP itself
advertises nothing about compaction, so the agent definition is what declares
it. bb hides the built-in /compact command for agents that do not. OpenCode
declares it; Cursor does not.
nativeReasoning is optional. Use it for ACP agents that accept reasoning via
session/set_config_option but do not advertise a thought_level config option
during model discovery. configId is the ACP config id to set,
supportedLevels controls the picker levels, defaultLevel controls the
picker default, and levelValues maps bb reasoning levels to the agent's ACP
config vocabulary when they differ. Hermes Agent uses this with
configId: "reasoning_effort".
For ACP-native agents, bb also uses a protocol thought_level config option
when the selected model advertises one. The selected reasoning level is applied
with session/set_config_option before the first prompt. Models without that
option keep agent-managed reasoning unless the provider launch spec declares
nativeReasoning. Cursor is intentionally separate: it encodes reasoning in
model ids discovered through modelCli, not in an ACP thought_level option.
Grok Build is also separate: it uses reasoningCli to launch
grok --reasoning-effort <level> agent stdio.
When an agent declares thought_level with no options, bb hides the reasoning
control. bb also hides it when none of the declared values map to a supported bb
reasoning level. Omitting thought_level keeps the agent-managed reasoning
fallback for agents that do not advertise this capability.
Custom ACP agents are supported only with the co-located daemon from the same machine as the server. A command path in server config is host-local and is not meaningful for a remote daemon.
Security note: command is arbitrary local code execution by design. Anyone who
can write ~/.bb/config.json can cause bb to run that command as the local user
when the provider is used. Treat config.json write access as the trust
boundary.
Custom Models
Register extra picker models by editing top-level customModels in
~/.bb/config.json. Use this for a model the provider accepts but does not
list, such as a non-public preview id. Like customAcpAgents, this list has
no set/unset CLI surface: edit the JSON, then run npx bb-app config refresh
or restart bb. bb-app config list prints the entries.
{
"customModels": [
{ "providerId": "claude-code", "model": "claude-example-preview" },
{
"providerId": "acp-my-agent",
"model": "my-proxy/my-model",
"displayName": "My Proxy Model"
}
]
}
providerId accepts a built-in provider id (codex, claude-code, pi,
acp-cursor) or any acp-* provider id: an installed-only plugin provider
such as acp-opencode, or a custom ACP agent's derived acp-<id>. displayName is
optional; bb derives the label from the model id when it is omitted. bb skips
an invalid entry with a warning and keeps the rest of the config.
Each entry appears in bb provider models <providerId> and in the model
picker after the provider's own catalog. The provider catalog wins on a model
id collision. The "Streamer mode" General setting
(bb settings general streamerMode true) hides every entry from these lists
until you turn it off again.
A customModels entry only makes the id selectable; the provider must still
accept it. Built-in providers such as claude-code and codex accept
unlisted ids. An ACP agent receives the id over the protocol at session start
and can reject it. OpenCode rejects a model that is not in its own catalog,
so do not pin OpenCode models here: add the model to the OpenCode config and
bb discovers it automatically.
An OpenCode "agent" (build, plan, or a custom primary agent) is a session
mode, not a model, so it does not belong in customModels. bb does not select
OpenCode agents; set the default agent in the OpenCode config instead.
Agent Instructions
bb can inject user-level and workspace-level agent instructions into every provider-backed thread's system prompt, alongside the skills convention.
For user-level defaults across projects, create AGENTS.md in the bb data dir:
<dataDir>/AGENTS.md
For repo-specific guidance, create .bb/AGENTS.md at the workspace root:
<workspace>/.bb/AGENTS.md
The file contents are appended to bb's standard agent instructions when a
provider session starts, so the guidance applies regardless of which provider
runs. When both files exist, <dataDir>/AGENTS.md is appended first and
<workspace>/.bb/AGENTS.md second. An empty or whitespace-only file is treated
as absent.
No agent loads .bb/AGENTS.md natively, and provider-native instruction files
(CLAUDE.md for Claude Code, a repo-root AGENTS.md for Codex) remain
provider-specific. bb reads the files above itself and injects them, so use them
for guidance you want every bb thread to receive regardless of provider.
Skills
User-level bb skills live under <dataDir>/skills/<name>/SKILL.md; for the
packaged app this is usually ~/.bb/skills. Project skills live under
<workspace>/.bb/skills/<name>/SKILL.md and override same-named user or built-in
skills. Running plugins contribute a third tier: every skills/<name>/SKILL.md
in an installed plugin (relocatable via the manifest's bb.skills field) is
auto-imported while the plugin is loaded — overridden by project and user
skills by name, overriding built-ins.
bb indexes each provider's native skill roots for that provider's / command
menu. The Skills page and bb skill list show native skills for Claude Code,
Codex, and Cursor.
| Provider | User roots | Project roots |
|---|---|---|
| Codex | ~/.agents/skills, $CODEX_HOME/skills | .agents/skills from the repository root to the current directory, plus .codex/skills |
| Claude Code | $CLAUDE_CONFIG_DIR/skills or ~/.claude/skills, plus enabled plugin skills | .claude/skills from the repository root to the current directory, plus enabled plugin skills |
| Pi | ~/.pi/agent/skills, ~/.agents/skills | .pi/skills and .agents/skills from the repository root to the current directory |
| Cursor | ~/.cursor/skills, ~/.agents/skills, ~/.claude/skills, ~/.codex/skills | The same four roots in the workspace |
| OpenCode | ~/.config/opencode/skills, ~/.claude/skills, ~/.agents/skills | .opencode/skills, .claude/skills, and .agents/skills from the repository root to the current directory |
| omp | The active ~/.omp/.../agent roots and supported Pi, Agents, Claude, Codex, and OpenCode roots | .omp/skills and the supported compatibility roots from the repository root to the current directory |
| Grok Build | $GROK_HOME/skills or ~/.grok/skills, plus ~/.agents/skills, ~/.claude/skills, ~/.cursor/skills | The same four roots from the repository root to the current directory |
| Hermes Agent | $HERMES_HOME/skills or ~/.hermes/skills | None |
OpenCode also uses $OPENCODE_CONFIG_DIR/skills when that variable exists.
Pi and omp use $PI_CODING_AGENT_DIR when that variable exists. omp also uses
$OMP_PROFILE or $PI_PROFILE to select its active profile root. Cursor and
Hermes can organize skills in category directories. bb scans those roots
recursively. Pi settings and packages can add skill paths. omp reads
skills.customDirectories from its YAML configuration. Hermes reads
skills.external_dirs from config.yaml.
Grok reads recursive paths from [skills].paths in config.toml. It also reads
enabled Grok and Claude-compatible plugin skills. Its Cursor and Claude
compatibility roots follow the related config and environment switches.
Multi-machine
Settings → Machines can enroll,
rename, and remove machines; project settings can add a path or clone source on
each machine; and thread creation can target any enrolled machine with a usable
source. The CLI equivalents are bb machine list, bb project create --machine <id-or-name> ..., bb project source add --machine <id-or-name> ..., and bb thread spawn --machine <id-or-name> ....
Multi-machine execution is independent of browser access. Tailscale and bb connect let another browser reach the bb server; multi-machine support lets that server dispatch work to non-primary host daemons. The Settings → Machines installer can use a paired bb connect account to route the daemon and its CLI back to the server. Machine credentials remain locally managed as described at the top of this document.
Each machine has a permission ceiling (maxPermissionMode, default full).
The server resolves every thread on that machine down to the ceiling, so a
paired sandbox machine can keep Full Access while a personal machine stays at
Approve for me or Accept Edits. A provider that supports no mode under the
ceiling is refused on that machine. Only an owner session sets it, on the machine
page (Settings → Machines → the machine, which also carries that machine's
projects, provider CLIs, update state, and rename/remove); it is deliberately
absent from the SDK and the bb CLI,
and machine credentials are rejected at both the bb connect gate and the
server. The boundary it defends is machine-to-machine: a process already
running on the server machine has the data directory and the server itself, so
it is trusted as the owner here exactly as it is for renaming or removing a
machine. The current value is readable through the host API and
bb machine list --json.
Machine installation and daemon protocol repair use the owning server as the
distribution source: /install/version reports the server package/protocol and
/install/bb-app.tgz serves its exact installable package. The installer falls
back to the npm registry only when the package route returns 404. It installs
the package under the machine's bb data directory rather than npm's system-wide
prefix, so enrollment needs neither sudo nor a global npm configuration.
Installed services enable --auto-update; remove that flag from the launchd
plist or systemd user unit and reload the service to opt out. Updates only move
to a newer server protocol, retry failures with a persisted exponential backoff
from 5 seconds to 5 minutes, and never downgrade a daemon. Settings → Machines
and bb machine retry-update <id-or-name> can bypass the current backoff after
a transient failure.
Thread splits
Thread splits enable up to eight panes in the app's multi-pane thread view and
its sidebar, menu, and keyboard split controls. Edge placement creates panes
through the eighth pane; at the eight-pane limit, opening a new thread with an
edge placement replaces the focused pane. Every pane header can temporarily
maximize that pane without unmounting or resizing the underlying split tree;
the same control restores the exact arrangement. Maximization follows focus and
newly opened panes, closing the maximized pane restores the surviving layout,
and both the split tree and maximized pane restore after reload. Compact
viewports show the ordinary single-page surface while preserving that desktop
layout state.
It also enables explicit split placement through
bb thread open <thread-id> --split right|down|left|top|replace and the matching
SDK request, plus pane presentation controls through
bb thread pane maximize|restore|toggle|spotlight|clear-spotlight [thread-id] and
sdk.threads.paneAction({ threadId, action }). Pane actions apply only when the
target thread is already open in a multi-pane app window; the response reports
how many connected clients received the broadcast. spotlight focuses the
target pane and persistently dims the others; clear-spotlight focuses it and
persistently restores undimmed splits.
bb connect
bb connect --code <code> --server https://<handle>.getbb.app pairs this bb
server for browser access at <handle>.getbb.app (claim a handle and copy the
command at https://getbb.app). Remote access is owned by the builtin
connect plugin (plugins/connect/): pairing redeems the code and stores
the durable credential in the plugin's kv storage (in bb.db), and the
plugin's background service holds the connect tunnel — dialing the gate,
proxying relayed requests to the server's own loopback (which serves the SPA
/api+/ws), and reconnecting with capped backoff. The tunnel therefore lives as long as the bb server runs (with the plugin enabled) and re-establishes on restart; there is no foreground client. Pair from a machine without an installed bb vianpx -p bb-app@latest bb connect ….bb connect statusshows the connect state and every share's host and URL;bb connect offdisconnects and clears the pairing. After pairing,bb connect expose <port>run from a thread shares that thread environment's enrolled host. Server-host URLs remainhttps://<server-label>--<port>.getbb.app; other machines usehttps://<machine-label>--<port>.getbb.appand proxy directly through the owning daemon. Outside a thread the command defaults to the server host;--host <name-or-id>overrides host resolution. Access requires the owner's getbb.app session (not a public link).bb connect unexpose <port>andbb connect sharesuse the same host resolution and accept the same--hostoverride. Their JSON rows includehostId,hostName,port, andurl;shares --jsonalso includes the resolvedhost. A machine without a live Connect enrollment fails fast with instructions to remove and re-add it in Settings → Machines. Disabling the plugin (bb plugin disable connect) cuts off all remote access;bb plugin enable connectrestores it.
The tunnel client lives in plugins/connect/; the CLI command is proxied to
the plugin, and Settings → Connect drives the plugin's rpc (including shared
ports).
Pairing the bb mobile app
The bb mobile app reaches a paired bb through the same connect route. It
enrolls as a connect machine — its own credential on the getbb.app account,
separate from the server's pairing secret and individually revocable — so
pairing starts from the bb, not from the phone. Both pairing surfaces sit
behind the mobileApp experiment (Settings → Experiments → Mobile app, or
bb settings experiment mobileApp true) until the app is generally available;
the connect plugin reads the experiment from /system/config on every call,
so a toggle applies without a plugin reload:
- Settings → Remote access → Add mobile device mints a one-time code and shows it as a QR code plus copyable text with a countdown.
bb connect machine-codeprints the same code, server URL, connect apex, and expiry;bb connect machine-code --jsonreturns{code, serverUrl, apex, expiresAt}(the QR encodes that JSON).
Scan or type the code in the mobile app. The code lasts 10 minutes and works
once. The phone then appears in the getbb.app dashboard machine list, where you
can revoke it; every enrollment takes one of the account's machine slots
(desktop apps, remote execution machines, and phones all count), so a
machine-limit error asks you to revoke an unused device first. Both surfaces
need the experiment on, the bb paired (bb connect --code …), and the connect
plugin enabled; with the experiment off the panel hides the section and
bb connect machine-code exits 1 with a pointer to the toggle.
Experiments
Experimental surfaces are changed in Settings → Experiments or with
bb settings experiment <key> <true|false>. Most start off; editMessages
starts on and its toggle is the opt-out.
The default-off changelogPreview experiment shows the latest release notes
as a compact, dismissible card on Settings → Updates.
The editMessages experiment is on by default and enables replacing an
eligible, accepted root user message in a Codex, Claude Code, or Pi thread,
including failed or incomplete turns. Turn it off to hide the editor. Grouped
multi-message requests are not yet editable. Opening the editor does not change
history; if the thread is running, submission stops the current turn and waits
for it to settle before atomically replacing that message and every later turn
while keeping workspace changes.
The mobileApp experiment turns on pairing for the bb mobile app: the
Add mobile device card under Settings → Remote access and the
bb connect machine-code command (see "Pairing the bb mobile app" above). It
is off by default while the app is in early access.
The providerSessionReaping experiment extends idle session release to every
restorable provider. BB releases those sessions after 30 idle minutes. The
daemon reads the setting before each five-minute maintenance pass. Active
turns, commands, agents, workflows, and monitors keep their sessions loaded.
The experiment does not gate release: BB releases idle Codex sessions with the
experiment off, which is the behavior it had before this setting.
The timelineWindowing experiment is off by default. When enabled, long
timelines and large expanded timeline details retain stable height-preserving
wrappers while mounting only rows near their active scrollport. Toggle it with
bb settings experiment timelineWindowing <true|false>.
Thread Timeline Window
A thread-timeline window is bounded by segment (user-message) count and by
event count. Segment count alone is a weak bound on work, because an agentic
turn can be thousands of events: a thread with 21 user messages and 21k events
used to reproject its entire history on every timeline request. That projection
is synchronous, so it blocked the server's event loop — which also delayed
/internal/session/events, the endpoint the host daemon awaits before every
dynamic tool call and before registering every interactive request. One slow
thread therefore slowed agent work on every thread on the host.
A window is capped at BB_FF_TIMELINE_WINDOW_EVENT_BUDGET events (default 1500) and returns however many whole turns fit. Older turns load automatically
as you scroll toward the top of the loaded window; a manual "Load older
messages" button remains on surfaces that render no scroll body, and after a
failed page so a broken fetch is retried on request rather than in a loop.
Nothing becomes unreachable — pagination still walks the full history, and the
head-state banners (goal, pending todos, running workflows, background
commands) are resolved by thread-scoped lookups rather than by scanning the
window, so a narrow window cannot drop them mid-session.
A turn larger than the whole budget is cut at the budget while it is running, so watching an agent work through a very long turn costs the budget per update rather than the whole turn; scrolling up loads the earlier part. Once the turn finishes it is rendered whole again, because a finished turn collapses into one summary row that two pages cannot each own — so the budget bounds a running turn and a long thread, but not a single finished oversized turn.
Raising the budget far above the default restores the previous unbounded-in-practice behavior; it is an operator escape hatch set at server start, not a product setting.
Timeline builds slower than 150ms log Thread timeline build blocked the event loop with a per-stage breakdown, and event-loop stalls over 500ms log Event loop stalled. Both log at info, so they are visible in ~/.bb/logs/ without
raising BB_LOG_LEVEL.
Plugins
Plugins are on by default. Builtin plugins, including connect, ship with bb;
user-installed plugins come from bb plugin install or the bundled official
store.
Plugin state lives under the data dir:
<dataDir>/plugins/<id>/data.db Per-plugin SQLite database
<dataDir>/plugins/<id>/secrets/ Secret settings and the plugin HTTP token
<dataDir>/plugins/<id>/logs/ bb.log output (plugin.log, JSONL, rotated
at 5MB; read with `bb plugin logs <id>`)
<dataDir>/plugins/git/, npm/ Managed installs for git:/npm: sources
<dataDir>/marketplaces/staging/ Throwaway checkouts a git: marketplace
refresh reads its manifest from, deleted
as soon as the catalog is stored
<dataDir>/skills-generated/ Server-generated skills (the
plugin-commands skill listing plugin CLI
commands, injected into agent threads)
BB's official plugins (GitHub, Docs, Memory, and Tasks) ship bundled
inside the app and install from the local bundled copy — no network, no remote catalog.
Discover them with bb plugin search or Extensions → Plugins → Browse; users
cannot add, remove, or configure the bundled official plugin set. Installed official
plugins are pinned to the bundled copy and update with BB app releases. Local
path installs remain available directly through bb plugin install ./path or
path:..., and direct npm:/git: installs stay supported.
Marketplace catalogs and their validated icon bytes live in the bb database,
not on disk. bb marketplace add|list|refresh|remove and Settings → Plugin
marketplaces manage them; the reserved bb-community marketplace comes from
BB_MARKETPLACE_URL and cannot be added or removed. Adding a marketplace
installs nothing, and removing one keeps its installed plugins as direct
installs.
Multi-plugin repositories
A repository can hold several plugins. Each plugin directory keeps its own
package.json and bb manifest; an optional .bb/plugins.json collection
manifest at the repository root indexes them:
{
"$schema": "https://getbb.app/schemas/plugins.schema.json",
"schemaVersion": 1,
"name": "acme-plugins",
"plugins": [
{ "name": "notes", "source": "./plugins/notes" },
{ "name": "status", "source": "./plugins/status" }
]
}
The file is strict: schemaVersion must be 1, names match
^[a-z0-9][a-z0-9-]*$, unknown fields and duplicate names are rejected, and
each source is a repository-relative directory starting with ./ — absolute
paths, .., empty segments, and the repository root itself are refused. An
invalid file is rejected whole. The manifest is an index only; it never
overrides a plugin's identity, branding, entry points, or engine ranges.
Install one plugin of the repository with
bb plugin install git:<url>[@<ref|semver-range>] --plugin <name> (resolves a collection
entry) or --subdirectory <relative-path> (the primitive, which needs no
collection manifest). Both flags work for path: sources and are mutually
exclusive. Installs from the same repository and commit share one cached
checkout, and the selected subdirectory is recorded with the install, so
bb plugin outdated, update, rollback, and remove act per plugin. A
repository that has a collection manifest and is not a plugin itself refuses
an unselected install and lists its entry names.
Plugin updates
Bundled builtin and official plugins update with BB app releases. For direct
git:/npm: installs, update application is manual: bb plugin outdated or
the "Check for updates" key on the Plugins page checks tracking sources, and
bb plugin update <id> / bb plugin update --all or the "Update x.y.z" pill
applies compatible candidates. The server also checks every installed plugin
every 6 hours (the first check runs when any plugin has no recorded check or
the oldest one is older than 6 hours), at most four plugins at a time, and a
manual check joins a sweep already in flight; a check only records what is
available and never installs or runs plugin code. There is no automatic plugin update
application or update audit feed. Reinstalling an already-installed managed plugin is
refused — use bb plugin update. Before activation bb snapshots the plugin
database, host-managed settings/storage/schedules, secrets, and registration.
A failed activation restores that snapshot and records the latest failure on
the plugin so it can be surfaced as needing attention.
Provider retry plugin
The builtin Provider retry plugin is enabled on fresh installations. It
automatically waits for structured Codex and Claude Code subscription-window
resets when the failed turn was accepted, the provider has stopped its own
retries, and the original execution settings remain available. Prior output or
tool activity does not block recovery. Recovery sends one agent-only
Please continue. turn on the existing provider conversation. Disable it
under Extensions → Plugins or with bb plugin disable provider-retry.
The maximumWait setting defaults to 6 hours; resets beyond that horizon are
not scheduled. Choose 24 hours or No limit under the plugin settings, or
configure it from the CLI:
bb plugin config provider-retry set maximumWait "24 hours"
Pending waits are coordinated by machine/provider subscription and live only
in the current server/plugin process. Restarting bb, reloading the plugin, or
disabling it clears the timers without changing the original failed thread. A
later 429 without a fresh provider rate-limit update can still inherit the last
blocked window during that process.
Inspect them with bb provider-retry status, or cancel one from its composer
banner or with bb provider-retry cancel <thread-id>. Run
bb provider-retry retry <thread-id> for a manual recovery, including credit
or spend-control limits that do not report a reset time.
Workflows plugin
The builtin Workflows plugin is disabled on fresh installations. Enable it
under Extensions → Plugins or with bb plugin enable workflows. Its six
settings accept base-10 integer strings through Extensions → Plugins or
bb plugin config workflows set <key> <value>:
| Key | Default | Allowed range | Behavior |
|---|---|---|---|
maxActiveRuns | 4 | 1–32 | Concurrent runs across the plugin; changes apply live. |
maxConcurrentAgents | 8 | 1–64 | Concurrent agent calls within one run. |
maxAgentCalls | 100 | 1–1000 | Total agent calls within one run. |
totalRunTimeoutMs | 86400000 | 60000–604800000 | Maximum total run duration in milliseconds. |
retentionDays | 30 | 1–3650 | Days to retain completed workflow data. |
maxNotificationBytes | 16384 | 1024–262144 | Maximum UTF-8 size of a completion notification. |
The five settings other than maxActiveRuns are snapshotted into each new run.
Settings changes do not require a plugin reload.
bb plugin install npm:<package>[@<version|tag|range>] requires npm on PATH
(packages are installed with --ignore-scripts). Git plugins also use npm with
lifecycle scripts disabled, so they may depend on third-party packages; bb
then builds both their server and frontend bundles. node_modules is
retained, because a dependency can load data files that bundling cannot
inline. A committed dist/ is always replaced by the bundles bb builds.
Dependency resolution and bundling run on install and update-apply only —
never on an update check, which reads the manifest and stops. An omitted npm
spec tracks the newest compatible stable release, ranges track within the
range, dist-tags track the tag, and exact versions are pinned. A bare HTTP(S)
Git repository URL or git:<url>[@<ref|semver-range>] requires git; an
omitted ref tracks the repository's default branch, explicit branches track
their head, and tags and commits are pinned. A semver range
(git:<url>@^1.2.0, or @semver:<range> to state the intent explicitly)
tracks the repository's [<tag-prefix>]vX.Y.Z release tags: bb installs the
highest release the range allows, excludes prereleases unless the range names
one, records the tag and the commit it pointed at, and refuses that tag later
if it moved. --tag-prefix <prefix> ranges over one plugin's tags in a
multi-plugin repository. A bare range that is also a literal branch or tag
name fails the install and asks for @semver: or @ref:. Local
path installs register the directory in place and never delete it. Builtin
plugins use builtin:<name> and ship with bb unless removed. Managed
(git:/npm:) installs
refuse plugins whose optional engines.bb or engines.bbPluginSdk ranges
do not match the running bb/SDK, or whose dist/*.meta.json plugin identity
does not match the package manifest; installing a non-builtin source whose
derived id collides with a builtin name (automations, connect,
custom-instructions, inline-vis, secrets, workflows) is also refused.
engines.bbPluginSdk is a floor, not a ceiling. bb reads the lowest version
the range allows and runs the plugin on any SDK at or above it within the same
major, so a caret range such as ^0.4.1 keeps working after the SDK moves to
0.4.3 or a later 0.x. Only a plugin that asks for a newer SDK than this bb
provides, or one pinned to a different major, is incompatible. Declare the
oldest SDK you need (>=0.4.3); a breaking plugin API change bumps the major.
The same tracking intent drives updates: bb plugin outdated checks for
compatible candidates (and reports blocked incompatible newer releases);
bb plugin update <id> / bb plugin update --all applies them. Pinned source
intent is never widened by update; remove and reinstall to choose a different
source intent. Dev builds (bb 0.0.0) do not enforce engines.bb and annotate
that on check results.
Update confirmation matches install (full-trust code; --yes skips; non-TTY
refuses without it). Plugins are full-trust code running inside the bb server
process: they can read all local bb data, including other plugins' secrets.
Startup Flags
Use launcher flags for per-run startup details:
npx bb-app --data-dir ~/.bb-test --server-port 48886 --host-daemon-port 48887
The server listens on 127.0.0.1 by default. Set
--server-bind-host 0.0.0.0 (or BB_SERVER_BIND_HOST=0.0.0.0) only when a
trusted network boundary must reach the listener directly. The public API is
unauthenticated and permits command execution and file reads, so never expose a
wildcard-bound server to an untrusted network. The only accepted bind hosts are
127.0.0.1 and 0.0.0.0; this startup-only setting is not available through
bb-app config.
The startup Server listening and app lines show the actual listener address.
With wildcard binding they show http://0.0.0.0:<port>, while bb's health check
and colocated host daemon continue to connect through 127.0.0.1. That local
connection does not narrow the listener. 0.0.0.0 exposes IPv4 interfaces only;
bb does not currently offer an IPv6 wildcard bind option.
The data directory is the root directory for all bb-managed state: the SQLite
database, logs, host identity, thread storage, custom themes (theme/,
including optional Pierre / VS Code pierre-dark.json and pierre-light.json),
and
plugins. It defaults to ~/.bb/ for the packaged app. The pnpm dev source launcher derives an isolated data
directory under ~/.bb-dev/<checkout-instance>/ from the checkout path. The
checkout instance id is the sanitized path to the checkout, relative to your
home directory, plus a short hash suffix. Use --data-dir to point packaged-app
instances at different data directories for fully isolated environments.
If the default ports are already in use, set explicit ports before starting:
npx bb-app --server-port 48886 --host-daemon-port 48887
The Settings → Machines installer assigns every enrolled standalone host daemon
a stable local API port so it can coexist with the desktop app and with daemons
enrolled to other servers. Atomic reservations under
~/.bb-machines/host-daemon-ports/ cover both default and custom
BB_DATA_DIR locations. Its generated command accepts --host-daemon-port <port> when an explicit port is required.
Source Development
For source development only, pnpm dev, pnpm start:worktree, and pnpm start
load the repo-root dotenv cascade. Add a repo-root .env only when you need to
override the defaults described above.
The standard dotenv-cli cascade
applies to source development. pnpm dev loads .env, .env.local,
.env.development, and .env.development.local, then overrides the instance
selectors (BB_DATA_DIR, server URL/port, host-daemon local API port, and Vite
port) with deterministic values derived from the checkout path. The SQLite
database path is always derived from BB_DATA_DIR. Both the main server and
Vite app bind to loopback by default; an explicit BB_DEV_APP_HOST still
overrides the Vite listener. Remote HTTP dev via BB_DEV_APP_HOST also requires
BB_SERVER_BIND_HOST=0.0.0.0 for realtime updates; the Tailscale Serve HTTPS
path avoids this because WebSocket traffic goes through the Vite proxy.
pnpm start:worktree loads the same development dotenv cascade and uses the
same checkout-specific data directory, server port, and host-daemon port. It
builds production artifacts and serves the frontend bundle from the main
server, so there is no separate Vite listener or hot reload. Telemetry remains
disabled for this source-development command. Its worktree data directory,
ports, inherited skills, listener host, absent Vite port, and telemetry policy
take precedence over conflicting values saved in that instance's config.json
or env.json.
pnpm start loads .env, .env.local, .env.production, and
.env.production.local.
Production startup from source uses the same launcher policy as the packaged
app while reading build outputs directly from apps/app, apps/server, and
apps/host-daemon. pnpm start:host-daemon continues to run the packaged
packages/bb-app/dist/bb-app.js host-daemon entrypoint. Source-only scripts do
not own production ports or data-dir defaults.
Source checkout commands such as pnpm bb, pnpm bb:dev, and pnpm reset
are thin wrappers around @bb/scripts. Those wrappers force NODE_ENV to the
intended mode so ambient shell state does not silently retarget bb.
Use pnpm reset or pnpm reset:dev to clear a data directory. These only
remove bb-managed state, not provider credentials.
BB_PROVIDER_BRIDGE_RECORD_DIR=<dir> in the host daemon's environment turns
on bridge record mode: every provider bridge writes the lines that cross its
runtime and provider wires as NDJSON under <dir>/<providerId>/<threadId>/.
It is a development and diagnostics knob, off by default, and never reaches a
provider child. See provider-bridge-protocol.md,
"Record mode", and debugging-and-qa.md. Raw recordings
can contain secrets; redact them with scripts/provider-recordings/redact.mjs
before you share them.