Everything on the Settings page is backed by a real, hand-editable
settings.json. That’s VS Code’s model, and for the same reason: a flat file of dotted keys is
unambiguous to edit, readable in a diff, and easy to copy between machines.
It also carries a few things the UI has no control for, like per-token theme colours.
Opening it
- Command palette (⌘⇧P) → Preferences: Open Settings (JSON), which opens the file in Bandura’s own editor.
- Settings → settings.json → Edit settings.json opens the same editor.
- Settings → settings.json → Show in folder reveals the file in Finder/Explorer, if you’d rather use your own editor.
The file lives in the app’s user-data directory. Changes apply live. Save, and the app picks them up, whether you saved from Bandura or from another editor. Bandura watches the file; there is no reload command and no restart.
“Live” means all of it, not just the chrome: a colour change repaints the app, the code editor, and the terminal at the same moment. Nothing needs to be closed and reopened to pick up a new palette.
What’s not in it
Your API keys and your licence key are deliberately not in this file.
- API keys live in your operating system’s encrypted store (Keychain on macOS, DPAPI on Windows, libsecret on Linux), one per provider (see Connect an AI provider).
- Your licence key lives in the app’s own config file, not here. It isn’t encrypted and
isn’t meant to be: a licence key has to stay portable, re-enterable on a new machine,
quotable in a support email. Keeping it out of
settings.jsonjust means a hand-edit can’t invalidate it.
So settings.json is safe to read over someone’s shoulder, paste into a bug report, or sync
between machines. No API key is in it.
Where every other piece of state lives, and what does or doesn’t cross the network, is on the security page.
The keys
Every key is listed here. Delete one and it returns to its default.
Appearance
| Key | Type | Default | What it does |
|---|---|---|---|
ui.theme | dark | light | high-contrast | "dark" | Colour theme for the whole app: chrome, editors, terminal. |
ui.accent | preset name or hex colour | "blue" | Presets: blue, emerald, violet, teal, neutral. Or any hex, e.g. "#ff7a59". |
ui.fontSize | number, 10 to 20 | 13 | Base UI and editor font size, in px. |
ui.fontFamily | jetbrains | system | menlo | monaco | courier | "jetbrains" | Monospace font for the editors and code surfaces. |
ui.density | comfortable | compact | "comfortable" | Row and padding density for lists, trees, and panels. |
ui.lineHeight | number, 0.9 to 1.4 | 1 | Multiplier on the UI line-height scale. Lower is tighter. |
ui.themeOverrides | object of token → colour | {} | Override individual colour tokens on top of the active theme. |
Notifications
| Key | Type | Default | What it does |
|---|---|---|---|
ui.errorToastsPersist | boolean | false | Keep failure notifications on screen until you dismiss them, instead of fading after ten seconds. Off by default, because a faded notification is still readable in the notifications bell. |
history.retentionRuns | number | 200 | How many runs to keep on disk. Each one stores its per-node payloads, so an unbounded history is a file that only grows. Applied when the next run is recorded, so lowering it takes effect at the next run rather than immediately. 0 keeps every run, which is what every build before 1.0.0-rc.8 did. |
updates.checkOnStartup | boolean | true | Ask Bandura’s public releases list, at most once a day, whether a newer version exists, and say so if one does. The request carries no identifier, no licence key, and nothing about your machine or your flows. Off, and Bandura makes no network request at all except to the AI provider you configured, downloads included. |
updates.autoDownload | boolean | false | Fetch a new version in the background as soon as one is found, instead of waiting for you to press Download. Off by default: a download spends your bandwidth and your disk without asking, so each update needs its own click, covering that version only. Fetching the file tells GitHub which file, so the platform, architecture and version. Windows and the Linux AppImage only: a .deb has no in-app update path, and macOS is sent to the download page until its builds are signed. |
updates.installOnQuit | boolean | true | Apply an update you already downloaded the next time you quit, rather than asking a second time. On by default, because it spends nothing, makes no network request, and applies only bytes you approved. Nothing is installed while you are working; restarting now is always a separate click. |
Editor
These drive the code editor, and most are named after the Monaco option they set, so the mapping needs no lookup table.
| Key | Type | Default | What it does |
|---|---|---|---|
editor.wordWrap | boolean | true | Fold long lines onto the next row instead of scrolling sideways. |
editor.tabSize | number, 1 to 8 | 2 | Spaces per indent level. A .aether file is YAML, where a literal tab is a parse error, so indentation is always spaces. |
editor.renderWhitespace | none | boundary | selection | trailing | all | "selection" | When to draw whitespace marks. trailing is the one that catches a stray space at the end of a YAML line. |
editor.stickyScroll | boolean | true | Pin the enclosing block (the nodes: key, the node id) to the top of the editor while you scroll a long flow. |
editor.columnSelection | boolean | false | Alt-drag selects a rectangle instead of whole lines, so a column of values can be edited in one gesture. Off by default because it changes what an ordinary drag does. |
editor.formatOnPaste | boolean | false | Re-indent pasted text to where it lands. Off by default, unlike VS Code: a pasted code: | block is JavaScript whose indentation is content, and re-indenting it would change what the flow runs. |
editor.minimap | boolean | false | Show the code minimap down the right edge. |
editor.bracketPairColorization | boolean | true | Tint matching brackets by nesting depth, which is what makes a deeply nested expression readable. |
editor.indentGuides | boolean | true | Draw vertical guides at each indent level and highlight the active one. |
editor.cursorStyle | line | block | underline | line-thin | block-outline | underline-thin | "line" | Shape of the caret. A block caret is far easier to find at low vision than a thin line. |
editor.cursorBlinking | blink | smooth | phase | expand | solid | "blink" | How the caret animates. Set solid to stop the blink: a blinking caret is a documented migraine and seizure trigger. |
editor.cursorSurroundingLines | number, 0 to 20 | 0 | Minimum lines of context kept visible above and below the caret while scrolling. |
editor.fontLigatures | boolean | false | Join character pairs such as != and => into single glyphs. Turn it off if the merged forms are harder to read. |
editor.letterSpacing | number, -1 to 3 | 0 | Extra space between characters, in px. A little tracking helps some dyslexic readers. |
editor.trimTrailingWhitespaceOnSave | boolean | false | Strip spaces at the end of every line on save, so they never show up as diff noise. |
editor.insertFinalNewlineOnSave | boolean | false | End every saved file with exactly one newline, which is what Git and most tooling expect. |
editor.screenReaderMode | auto | on | off | "auto" | auto detects a reader and is usually right. Set on if yours announces the wrong line, off if the editor behaves as though one is attached when none is. |
editor.accessibilityPageSize | number, 10 to 500 | 40 | How many lines the editor exposes to a screen reader at once. The stock 10 is too few to follow a flow; very large values slow the editor down. |
AI
The first three are also editable from Settings → AI, which is the easier route because it stores the API key alongside them. See Connect an AI provider.
| Key | Type | Default | What it does |
|---|---|---|---|
ai.provider | anthropic | gemini | openai-compatible | "anthropic" | Which provider to use. openai-compatible covers OpenAI, Groq, OpenRouter, Together, Ollama and LM Studio, and needs ai.baseUrl. |
ai.model | string | "" | Model id for chat, generation, and ai-action. Empty falls back to the provider’s own default; an OpenAI-compatible endpoint has none, so name one. |
ai.baseUrl | string | "" | Endpoint for the OpenAI-compatible provider, e.g. "http://localhost:11434/v1". Ignored for Anthropic; for Gemini it overrides the public endpoint. |
ai.maxTokens | number, 256 to 32000 | 4096 | Cap on tokens the model may generate per request. |
ai.maxSteps | number, 1 to 40 | 16 | Cap on tool round-trips in one agent chat turn, to stop a runaway loop. Diagnosing a failing flow and proposing a fix takes about six round-trips, so a value under ten tends to cut the assistant off mid-task. |
ai.inlineCompletion | boolean | false | AI ghost-text suggestions as you type in the .aether editor. Off by default: turning it on sends the text around your cursor to your AI provider on every pause in typing, billed to your own key. |
ai.persistChatHistory | boolean | true | Keep chat conversations between sessions, so one can be reopened after a restart. Stored locally in chat.db. Turning it off stops new conversations being written and hides saved ones; it does not delete what is already there. |
ai.historyTokenBudget | number, 1000 to 100000 | 12000 | Roughly how many tokens of past conversation to resend with each message. Older turns beyond this are left out of the request; they stay in your saved history either way. |
ai.autoCompact | boolean | true | When a conversation outgrows the budget above, replace its oldest turns with a short summary instead of dropping them. Costs one extra request when it happens, and falls back to a plain summary if that request fails. |
ai.contextRetrieval | boolean | false | Search your workspace on every message and attach the most relevant parts. Off by default: it adds tokens to every message, billed to your own key. The assistant can still search on its own when it decides it needs to. Searching is local; credential-shaped text is never indexed. |
How it behaves when you get it wrong
The file is yours, so Bandura reads it defensively rather than rewriting it:
- Comments are allowed.
//and/* … */both work, and the generated file uses them to document every key inline. A//inside a string survives, so a base URL like"http://localhost:11434/v1"is not half a comment. Trailing commas are not allowed, though: comments are the one extension to strict JSON here. - Delete a key to restore its default. There’s no “reset” to hunt for; an absent key is the default.
- An invalid value degrades to its default, never a crash and never a silent rewrite of your
file. What you see is the setting snapping back to the default; the offending line stays in
your file, exactly as you typed it. A number outside its range is clamped to the nearest legal
value instead, because
21when the max is20is a preference and200is a typo. - A key Bandura doesn’t recognize is kept, not deleted. A typo stays visible, and a setting from a newer version survives a downgrade. Should the app ever rewrite the file itself, those keys are re-emitted verbatim under a comment saying so.
- One bad entry in
ui.themeOverridesis dropped on its own, so a single typo doesn’t discard a hand-tuned palette.
If the file doesn’t parse at all
A JSON syntax error is a different case from a bad value, and it’s handled conservatively:
- The app runs on defaults until it parses again. It doesn’t refuse to start, and it doesn’t “fix” the file for you.
- Your file is not touched. In the rare case where Bandura itself has to write settings while
the on-disk copy is unparseable, it first copies it to
settings.json.bak, so the version you were mid-way through editing is still there. - Saving from inside Bandura validates first. Edit settings.json refuses to write text that isn’t valid JSONC and shows the parse error inline, so the editor can’t be the thing that breaks the file. It also warns if the file changed on disk since you opened it.
- Every write is atomic (written to a temporary file and renamed into place), so an interrupted
save can’t leave a half-written
settings.json.
Theme overrides
ui.themeOverrides maps colour token names to colours, applied on top of whichever theme
ui.theme selects:
{
"ui.theme": "dark",
"ui.themeOverrides": {
"bg": "#101010",
"node-request": "#7aa2f7"
}
}
Values must be hex, rgb(), or hsl(). Those go straight into CSS custom properties, the
code editor’s theme, and the terminal palette, so anything more exotic would break one of the
three. There are 22 token names; anything else is ignored, and so is a value that isn’t a
colour. In both cases only that one entry is dropped, not the whole object.
Note the asymmetry with ui.accent directly above it: the accent is hex-or-preset only.
Its hover shade and the text colour on filled accent surfaces are computed from the colour’s
channels, which needs hex.
The full token list, what each one paints, and a worked example are on Customize the theme.
Example
{
// A local model, no key, nothing leaves the machine.
"ai.provider": "openai-compatible",
"ai.baseUrl": "http://localhost:11434/v1",
"ai.model": "llama3.1",
"ui.theme": "light",
"ui.accent": "#ff7a59",
"ui.density": "compact"
}
Every other key falls back to its default because it isn’t there.
Related: Settings & appearance · Customize the theme