This guide is the architectural and operational reference for Bruno, the open-source, offline-first API client. It is written for the people who deploy Bruno into a team's workflow, integrate the Bruno CLI into CI/CD, secure script execution, package and self-distribute the app, or contribute to the codebase.
Where the User Guide covers using Bruno, this guide covers how Bruno is built, configured, secured, and operated. It is deliberately exhaustive: it is intended to give an engineer (or an AI) enough grounding to operate, extend, or reconstruct the system.
Note
Bruno is a monorepo containing two shipping products — the desktop app (
bruno, an Electron application, version 2.0.0) and the Bruno CLI (@usebruno/cli, version 1.16.0) — plus fourteen internal packages that the two products share. This guide is organized around that structure.
Table of Contents
- Executive Summary
- Architecture Overview
- The Monorepo: Package Reference
- The Bru Language Pipeline
- The Desktop App: Electron Architecture
- The Request Engine
- The Script Sandbox
- System Requirements
- Configuration Reference
- Security
- Build and Release
- Operating the CLI in CI/CD
- Development and Contributing
- Testing Strategy
- Potential Improvements
- Quick Reference
1. Executive Summary
Bruno is an API client whose architecture is shaped by one principle: collections are plain-text files on the user's disk, never in a cloud. Everything else follows from that.
| Attribute | Value |
|---|---|
| Product type | Desktop API client (Electron) + headless CLI runner |
| Desktop app version | 2.0.0 (package bruno) |
| CLI version | 1.16.0 (package @usebruno/cli, binary bru) |
| License | MIT (with paid commercial editions for advanced features) |
| Languages | JavaScript and TypeScript |
| Repository layout | npm-workspaces monorepo, 16 packages |
| Desktop stack | Electron ~37.6.1, React 19, Redux Toolkit, CodeMirror, rsbuild |
| CLI stack | Node.js (yargs, axios), distributed via npm and Docker |
| Protocols | HTTP/HTTPS, GraphQL, gRPC, WebSocket, SSE |
| Persistence | Plain-text .bru (and .yml/OpenCollection) files on disk |
| Collaboration model | Git (collections are version-controlled folders) |
| Network posture | Offline-first; no cloud sync, ever |
The system has three conceptual layers:
- The file format and language (
@usebruno/lang,@usebruno/filestore,@usebruno/schema) — parse and serialize.bru/.ymlcollections to and from an in-memory JSON model. - The execution engine (
@usebruno/requests,@usebruno/js,@usebruno/common,@usebruno/query) — send requests across protocols, run user scripts in a sandbox, evaluate assertions and tests. - The surfaces (
bruno-electron+@usebruno/appfor the desktop GUI;@usebruno/clifor the command line) — the two products that present the engine to users.
Important
A future maintainer's mental model should be: the same parse → resolve → execute → assert pipeline runs in both the desktop app and the CLI. The CLI is not a reimplementation; it composes the same shared packages the app does. This is why a collection behaves identically in both.
2. Architecture Overview
High-level component diagram
┌───────────────────────────────────────────────┐
│ Shared packages │
│ │
┌────────────┐ │ @usebruno/lang (.bru grammar, ohm-js) │
│ Desktop │ │ @usebruno/filestore(read/write .bru & .yml) │
│ App │◄───────►│ @usebruno/schema (collection schema) │
│ (Electron) │ │ @usebruno/converters (Postman/OpenAPI/WSDL) │
└────────────┘ │ @usebruno/requests (HTTP/gRPC/WS/SSE engine) │
▲ │ @usebruno/js (script sandbox: QuickJS / │
│ │ Node VM, bru/req/res API)│
│ │ @usebruno/query (JSON query for res(...)) │
│ │ @usebruno/common (interpolation, runner, │
▼ │ shared utils) │
┌────────────┐ │ @usebruno/graphql-docs, @usebruno/toml │
│ Bruno CLI │◄───────►│ @usebruno/schema-types (TS types) │
│ (bru) │ │ │
└────────────┘ └───────────────────────────────────────────────┘
│
▼
Filesystem collection (folder of .bru files) ◄──► Git
The desktop app's two processes
The Electron app is split, as all Electron apps are, into:
- Main process (
packages/bruno-electron) — a Node.js process with full OS access. It owns windows, the application menu, the filesystem, the network stack, file watchers, Git operations, secret encryption, and all privileged work. It exposes capabilities to the UI through IPC handlers. - Renderer process (
packages/bruno-app) — a React 19 single-page application (Redux Toolkit for state, CodeMirror for editors) that renders the UI. It cannot touch the filesystem or network directly; it asks the main process via IPC.
A preload script bridges the two via Electron's contextBridge, exposing a narrow, explicit ipcRenderer API to the renderer.
The CLI's single process
The CLI (packages/bruno-cli) is a plain Node.js program. It reads the collection from disk, builds the same in-memory model, runs each request through the shared engine, evaluates tests/assertions, and writes reports. It has no GUI and no main/renderer split.
Note
Because the file format, request engine, and script sandbox are all in shared packages, the CLI and app cannot drift in behavior the way two separate codebases would. The "single source of truth" is the shared package set, not either product.
3. The Monorepo: Package Reference
The repository is an npm-workspaces monorepo. The root package.json declares the workspaces; each package under packages/ is independently versioned and (for the libraries) independently publishable to npm.
| Package | Version | Role |
|---|---|---|
bruno (bruno-electron) |
2.0.0 | The desktop app — Electron main process, IPC, windows, watchers, OS integration |
@usebruno/app (bruno-app) |
2.0.0 | The desktop renderer — React 19 SPA (Redux, CodeMirror, rsbuild build) |
@usebruno/cli (bruno-cli) |
1.16.0 | The command-line runner (bru) |
@usebruno/lang (bruno-lang) |
0.12.0 | The Bru markup grammar and parsers (v1 + v2), built on ohm-js and arcsecond |
@usebruno/filestore (bruno-filestore) |
0.1.0 | Read/write collections in .bru and .yml (OpenCollection) formats |
@usebruno/schema (bruno-schema) |
0.7.0 | The collection data schema (Yup-based validation) |
@usebruno/schema-types (bruno-schema-types) |
0.0.1 | TypeScript types for the schema and config |
@usebruno/requests (bruno-requests) |
0.1.0 | The network layer: HTTP, gRPC, WebSocket, proxy resolution, cookies, TLS, scripting send |
@usebruno/js (bruno-js) |
0.12.0 | The script sandbox and the bru/req/res scripting API; QuickJS + Node VM runtimes |
@usebruno/query (bruno-query) |
0.1.0 | JSON query engine powering the callable res('path') API |
@usebruno/common (bruno-common) |
0.1.0 | Variable interpolation, the runner summary, shared utilities |
@usebruno/converters (bruno-converters) |
0.1.0 | Importers: Postman, Insomnia, OpenAPI/Swagger, WSDL → Bruno |
@usebruno/graphql-docs (bruno-graphql-docs) |
0.1.0 | The in-app GraphQL schema docs explorer |
@usebruno/toml (bruno-toml) |
0.1.0 | TOML serialization helpers |
@usebruno/tests (bruno-tests) |
0.0.1 | A test API server + sample collections used to exercise the CLI/app |
@usebruno/docs (bruno-docs) |
0.0.1 | Internal docs package |
Tip
When tracing a behavior, start from the package that owns it: parsing →
bruno-lang; file I/O →bruno-filestore; networking →bruno-requests; scripting →bruno-js; variable resolution →bruno-common; imports →bruno-converters.
Dependency shape
The libraries form a layered graph. bruno-lang and bruno-schema sit at the bottom (no Bruno deps). bruno-filestore depends on bruno-lang. bruno-js depends on bruno-common, bruno-query, and bruno-requests (for the scripting send path). The two products (bruno-electron, bruno-cli) sit on top and pull in lang, filestore, js, requests, common, converters, and schema.
The shared libraries are built with Rollup (dual CJS/ESM output with .d.ts types); the renderer is built with rsbuild; tests run on Jest; end-to-end tests run on Playwright.
4. The Bru Language Pipeline
The .bru format is the heart of Bruno's "files on disk" model. It is defined as a formal grammar in @usebruno/lang.
Grammar engine
The v2 parser (packages/bruno-lang/v2/src/bruToJson.js) is built with ohm-js, a PEG-style parsing toolkit. The grammar defines the complete .bru block vocabulary and a semantics layer maps the parse tree into the JSON request model. A separate collectionBruToJson.js grammar handles collection-level .bru (the bruno.json/collection settings analog). A legacy v1 parser (arcsecond-based) remains for older files.
Block taxonomy
The grammar recognizes three structural block kinds (dictionary, text, list) and a fixed set of named blocks:
BruFile = (meta | http | grpc | ws | query | params | headers | metadata
| auths | bodies | varsandassert | script | tests | settings
| docs | example)*
meta—name,type(defaults tohttp),seq(defaults to1).- HTTP method blocks —
get,post,put,delete,patch,options,head,connect,trace, andhttp(custom method). Each carriesurlplus per-request fields. grpc,ws— gRPC and WebSocket request blocks.query,params:path,params:query— parameters.headers,metadata— header and metadata dictionaries.auths— one ofauth:awsv4|basic|bearer|digest|ntlm|oauth1|oauth2|wsse|apikey, plus OAuth 2.0 additional-parameter sub-blocks (auth:oauth2:additional_params:*).bodies— one ofbody:json|text|xml|sparql|graphql|graphql:vars|form-urlencoded|multipart-form|file|grpc|ws, or barebody.vars:pre-request,vars:post-response,assert— variables and assertions.script:pre-request,script:post-response,tests,docs,example— text blocks.settings— per-request settings (parsed and type-coerced:encodeUrl/followRedirectsto booleans,timeout/maxRedirects/keepAliveIntervalto numbers;timeoutmay be the literalinherit).
Encoding conventions (semantics)
The semantics layer implements several conventions that maintainers must know:
| Syntax | Meaning |
|---|---|
~key: value |
The entry is disabled (enabled: false) but preserved in the file |
@name: value (in a vars block) |
A local variable (local: true), not persisted to the environment |
@file(path) (in multipart/file bodies) |
A file reference; only the path is stored |
@contentType(type) |
Pins a content type on a multipart/file field |
'''…''' |
A multiline value; the parser dedents by a fixed indent |
@annotation(arg) on a pair |
Pair-level annotations (decorators), captured under a hidden symbol key |
The OAuth 2.0 semantics are notable: the parser branches on grant_type and emits a different normalized object per grant (password, authorization_code, client_credentials, implicit), each with sensible defaults (credentials_placement: body, token_placement: header, auto_fetch_token: true, etc.).
Filestore: format abstraction
@usebruno/filestore sits above the language and abstracts the on-disk format. It supports both bru and yml (OpenCollection) formats behind a single API (parseEnvironment, parseDotEnv, format-specific read/write). When a caller requests an unsupported format, it throws Unsupported format: <format>. A worker (BruParserWorker) offloads parsing to a worker thread for large collections.
Note
The dual-format design (
.bruand OpenCollection.yml) is why the CLI'simportcommand takes a--collection-formatflag, and why the filestore is the single chokepoint for all serialization. Add a new on-disk format here, not in the products.
5. The Desktop App: Electron Architecture
The desktop app lives in packages/bruno-electron (main process) and packages/bruno-app (renderer). The main entry point is packages/bruno-electron/src/index.js.
Window and process configuration
The main process creates a single BrowserWindow with:
nodeIntegration: true,contextIsolation: true, apreload.jsbridge, andwebviewTag: true.- A minimum size of 700×400, persisted bounds and maximized state (via a window-state store).
- Platform-specific chrome:
hiddenInsettitle bar on macOS,hiddenon Windows, frameless on Linux. - A saved zoom level (50–150%) restored on launch and adjustable via menu, shortcuts, and IPC.
In production the renderer is loaded from a bundled web/index.html; in development it loads from a local dev server (default port 3000, overridable with BRUNO_DEV_PORT).
Single-instance lock and deep links
Bruno enforces a single running instance by default (app.requestSingleInstanceLock()), focusing the existing window if a second instance is launched. This can be disabled by setting DISABLE_SINGLE_INSTANCE=true. Bruno registers itself as the handler for the bruno:// protocol, enabling deep links that open collections or requests; protocol URLs are parsed from the launch arguments and from open-url/second-instance events.
IPC handler registry
All privileged capabilities are exposed to the renderer through IPC handler modules, registered once the window is ready:
| IPC module | Responsibility |
|---|---|
ipc/network |
Sending HTTP/gRPC/WS requests, the request lifecycle |
ipc/collection |
Opening, reading, writing collections and items |
ipc/filesystem |
File and directory operations |
ipc/preferences |
Reading/writing app preferences |
ipc/workspace |
Workspace and global-environment management |
ipc/global-environments |
Global environment CRUD |
ipc/apiSpec |
API spec (OpenAPI) handling |
ipc/openapi-sync |
Syncing collections against an OpenAPI spec |
ipc/git |
Git operations (via simple-git) |
ipc/snapshot |
Collection snapshots |
ipc/system-monitor |
System resource monitoring |
ipc/notifications |
In-app notifications |
ipc/terminal |
An embedded terminal (via node-pty) |
ipc/ai |
AI-assisted features (via the Vercel AI SDK with Anthropic/OpenAI providers) |
Note
The presence of
ipc/aiand the@ai-sdk/anthropic/@ai-sdk/openaidependencies indicate optional AI-assisted features in the desktop app. These are opt-in and require the user's own provider credentials; they do not run unless invoked.
File watchers
The main process runs several chokidar-based watchers that keep the UI in sync with on-disk changes (essential because collections are files that Git, editors, or teammates may change):
| Watcher | Watches |
|---|---|
CollectionWatcher |
Collection files and folders |
WorkspaceWatcher |
Workspace configuration |
ApiSpecWatcher |
API spec files |
DotEnvWatcher |
The collection's .env file |
On quit, the app deliberately awaits watcher shutdown (with a 2-second cap) before exiting, to avoid a race where native watcher cleanup outlives the process and triggers a "quit unexpectedly" dialog on macOS. It also flushes the cookie jar and kills embedded terminals on quit.
The preload bridge
preload.js uses contextBridge.exposeInMainWorld to expose a minimal API to the renderer: an ipcRenderer object (invoke, send, on, getFilePath, openExternal) and an isPlaywright flag. Event listeners deliberately strip the Electron event argument (which carries sender) before handing data to the renderer.
External link handling
The app refuses to navigate the main window to external URLs. will-redirect and setWindowOpenHandler intercept http(s) URLs and open them in the user's default browser instead of inside the app — a deliberate security boundary.
6. The Request Engine
@usebruno/requests is the protocol layer shared by both products. It is responsible for actually sending requests and resolving the network environment around them.
Protocols
- HTTP/HTTPS — built on
axios(pinned to1.13.6across the monorepo via anoverridesentry). - gRPC — via
@grpc/grpc-jsand@grpc/proto-loader, with reflection support (grpc-js-reflection-client). - WebSocket — via
ws. - SSE — server-sent event streaming.
TLS and certificates
The engine supports custom CA certificates (--cacert, with --ignore-truststore to use only the custom CA), client certificates (PEM cert/key pairs or PKCS#12 .pfx), and reading the system trust store (system-ca, NODE_EXTRA_CA_CERTS). Errors are explicit (Invalid custom CA certificate path, certFilePath is required for cert type, etc.).
Proxy resolution
Bruno resolves proxies in priority order: collection-defined proxy, then system proxy. System proxy detection is platform-specific, implemented in network/system-proxy/utils/{linux,macos,windows}.ts (gsettings on Linux, scutil --proxy on macOS, registry on Windows), with PAC (proxy auto-config) support via pac-resolver. The CLI fetches the system proxy once per run and caches it; --noproxy disables all proxy use.
Shell environment
The CLI and app call initializeShellEnv() (via shell-env) at startup to capture the user's shell environment variables — important when Bruno is launched from a GUI or cron context that doesn't inherit the interactive shell's PATH and proxy variables.
Secrets integrations
The engine includes a HashiCorp Vault client (node-vault, configured via VAULT_ADDR, VAULT_TOKEN, VAULT_NAMESPACE) for sourcing secrets, and AWS credential providers (@aws-sdk/credential-providers) for AWS SigV4 auth.
7. The Script Sandbox
User scripts (pre-request, post-response, tests, vars) are arbitrary JavaScript. @usebruno/js runs them in one of two sandboxes and exposes the bru/req/res API. This is the single most security-relevant subsystem.
Two runtimes
| Mode | Runtime | Isolation | How selected |
|---|---|---|---|
| Safe (default) | QuickJS (quickjs-emscripten, a WASM build of QuickJS) |
Strong — no host filesystem, no Node internals | default; --sandbox safe |
| Developer | Node.js vm |
Weak — full Node.js capabilities | --sandbox developer |
getJsSandboxRuntime(sandbox) maps safe → quickjs and anything else → nodevm. bru.isSafeMode() returns true when the runtime is QuickJS.
Warning
The two runtimes are not equivalent in trust. QuickJS (safe) is a true sandbox — scripts run in an isolated WASM engine with only the shims Bruno provides. The Node VM (developer) is not a security boundary — a script can reach Node's
require, the filesystem, child processes, and the network. Default to safe mode for any shared or untrusted collection. Document developer mode as a "trusted collections only" feature.
The QuickJS shims
Because QuickJS lacks Node and browser globals, @usebruno/js ships shims under sandbox/quickjs/shims/ — for example a crypto-utils shim implementing getRandomValues with explicit bounds checks (ArrayBufferView byte length exceeds 65536, typed-array validation). The sandbox libraries must be bundled before the app runs (npm run sandbox:bundle-libraries --workspace=packages/bruno-js); the main process throws JS Sandbox libraries have not been bundled yet in development if they are missing.
The scripting API surface
Three objects are injected into every script:
bru(bru.js) — the run context: variable get/set across all scopes,sendRequest,runRequest, cookies, flow control (runner.skipRequest/stopExecution/setNextRequest),sleep,interpolate,isSafeMode, andutils.minifyJson/minifyXml. Variable names are validated against^[\w-.]*$; persisted env vars must be strings.req(bruno-request.js) — the mutable outgoing request: URL/method/headers/body getters and setters,getAuthMode,headerList(a case-insensitivePropertyList),onFail,setTimeout,setMaxRedirects,disableParsingResponseJson.res(bruno-response.js) — the response: status/headers/body/timing,getSize, a read-onlyheaderList, and a callable form (res('json.path')) backed by@usebruno/query.
The PropertyList hierarchy (readonly-property-list.js → property-list.js → cookie-list.js/header-list.js) provides Postman-compatible collection APIs for headers and cookies, with dynamic (live-reading) and static modes.
Inbuilt libraries
Scripts can require a curated set of bundled libraries: axios, chai/chai-string (assertions), crypto-js, jsonwebtoken, cheerio, ajv/ajv-formats/tv4 (JSON Schema), lodash, moment, uuid, nanoid, xml2js, btoa/atob, node-fetch, and @faker-js/faker. A collection may declare its own package.json to make additional libraries available.
Error formatting
@usebruno/js invests heavily in script error reporting (utils/error-formatter.js): it maps runtime errors back to the correct line in the original .bru or .yml script block (findScriptBlockStartLine, adjustLineNumber, getSourceContext, adjustStackTrace) so users see the failing line in their script, not in Bruno's wrapper.
8. System Requirements
Desktop app (end-user machines)
| Requirement | Detail |
|---|---|
| OS | macOS (Apple Silicon + Intel), Windows, Linux (AppImage/.deb/.rpm/Snap/Flatpak) |
| Memory | Standard Electron footprint; large collections/responses increase usage |
| Disk | Small app footprint; collections are user files |
| Network | None required (offline-first); needed only to call the APIs you test |
CLI (build agents / developer machines)
| Requirement | Detail |
|---|---|
| Node.js | Node 18+ recommended; official Docker images use Node 22 |
| Install | npm install -g @usebruno/cli, or Docker (usebruno/cli) |
| Architectures (Docker) | linux/amd64, linux/arm64 |
Build environment (contributors)
| Requirement | Detail |
|---|---|
| Node.js | A current LTS (the toolchain targets Node 22 types) |
| Package manager | npm (workspaces) |
| Electron | ~37.6.1 (dev dependency of bruno-electron) |
| Build tools | Rollup (libs), rsbuild (renderer), electron-builder (packaging) |
9. Configuration Reference
Bruno is configured through three distinct mechanisms: collection configuration (on disk), app preferences (per user), and environment variables (process-level). There is no central server config — by design.
Collection configuration
A collection's root holds one of:
bruno.json— the classic collection config (name, version, type, scripts/proxy/clientCertificates settings).opencollection.yml— the OpenCollection YAML format.
If neither is present, the engine raises The collection is not valid (neither bruno.json nor opencollection.yml found).
Client certificate configuration can be supplied per-run via --client-cert-config <file>, a JSON file shaped as:
{
"enabled": true,
"certs": [
{ "domain": "api.example.com", "type": "cert", "certFilePath": "client.pem", "keyFilePath": "client.key" }
]
}
Workspace configuration
A workspace.yml file defines a workspace and its collections. Workspaces enable global environments (stored under the workspace's environments/ folder). The CLI auto-detects the workspace by walking up from the collection directory; --workspace-path overrides it. Validation errors are explicit (Invalid workspace: workspace.yml not found, Workspace must have a valid name, etc.).
Per-request settings
The settings block (parsed in bruno-lang) controls request behavior:
| Setting | Type | Default | Effect |
|---|---|---|---|
encodeUrl |
boolean | false |
URL-encode the request URL |
timeout |
number | inherit |
0 |
Request timeout (ms); 0 = none; inherit = use parent |
followRedirects |
boolean | (unset → engine default) | Follow HTTP redirects |
maxRedirects |
number | (unset) | Max redirects to follow |
keepAliveInterval |
number | (unset) | Keep-alive interval |
App preferences (desktop)
The desktop app stores user preferences (via electron-store), including display settings such as display.zoomPercentage (clamped 50–150). Preferences are read/written through ipc/preferences and persisted in the app's user-data directory.
Environment variables
These process-level variables affect Bruno's behavior:
| Variable | Scope | Effect |
|---|---|---|
ELECTRON_USER_DATA_PATH |
desktop (dev) | Overrides Electron's userData directory |
DISABLE_SINGLE_INSTANCE |
desktop | true disables the single-instance lock |
BRUNO_DEV_PORT |
desktop (dev) | Renderer dev-server port (default 3000) |
DISABLE_SAMPLE_COLLECTION_IMPORT |
desktop | Skip importing the sample collection on first run |
NODE_EXTRA_CA_CERTS |
both | Additional CA certificates for TLS |
VAULT_ADDR / VAULT_TOKEN / VAULT_NAMESPACE |
both | HashiCorp Vault secret sourcing |
PLAYWRIGHT |
testing | Set by the E2E harness; exposed to the renderer as isPlaywright |
DOTENV_PATH |
build | Path to the dotenv file used by the electron-builder config |
APPLE_ID / APPLE_ID_PASSWORD |
build | macOS notarization credentials |
DOTENV at collection root (.env) |
both | Loaded into process.env for the collection run |
Note
A collection-root
.envis read at run time (in the CLI, inrun.js) and merged intoprocess.env, making its keys available as{{process.env.KEY}}. This is the recommended secrets path; the file should be git-ignored.
10. Security
Security in Bruno centers on three areas: script execution, secret handling, and the network/renderer boundary. The offline-first stance is itself a security feature.
Script execution
This is covered in depth in The Script Sandbox. The essentials for an administrator:
- Safe mode (QuickJS) is the default and is a real sandbox. Untrusted/shared collections should always run in safe mode.
- Developer mode (Node VM) is not a security boundary. It grants scripts full Node.js access. Restrict it to collections your team authors and trusts. In CI, prefer
--sandbox safeunless a specific collection genuinely needs Node APIs.
Caution
Treat a Bruno collection like source code you are about to execute. A
script:pre-requestortestsblock in a collection from an untrusted source can run arbitrary JavaScript. In developer mode that means arbitrary code on the host. Review third-party collections before running them, and keep CI runs in safe mode.
Secret handling
.envat the collection root keeps secrets out of the committed.brufiles; reference them as{{process.env.NAME}}. The.envfile must be git-ignored.- Encrypted secret storage (desktop). The main process encrypts sensitive values using AES-256 and Electron's
safeStorage(utils/encryption.js), so secret environment values can be stored without being written in plain text into collection files. Decryption failures are explicit (AES256 decryption failed,SafeStorage decryption failed,Decrypt failed: Invalid algo). - HashiCorp Vault integration lets teams source secrets at run time rather than storing them at all.
The renderer/network boundary
- Content Security Policy. The main process sets a strict CSP:
default-src 'self', scripts limited to'self' data:, connections limited to'self'plus*.posthog.com(analytics). Images are allowed broadly (includinghttp:) so that markdown editors can embed linked images — a deliberate, documented exception. - External navigation is blocked. The window never navigates to external sites;
http(s)links open in the user's default browser viashell.openExternal.setWindowOpenHandlerdenies in-window popups. - Context isolation is enabled, and the preload bridge exposes only a narrow IPC surface.
Network posture
Bruno is offline-first: it has no cloud sync and does not transmit your collections anywhere. The only outbound connections Bruno initiates on its own are: the APIs you explicitly call, optional product analytics (PostHog, constrained by the CSP), and optional AI features (only when invoked, using your own provider keys). TLS verification is on by default; disabling it (--insecure) is an explicit, per-run choice.
Telemetry
Optional product analytics use PostHog (posthog-node; configured via NEXT_PUBLIC_POSTHOG_API_KEY). The CSP confines analytics traffic to *.posthog.com. No collection contents are sent.
11. Build and Release
Building the shared libraries
Each library builds with Rollup into dual CJS/ESM with type declarations. The root package.json exposes per-package build scripts:
npm run build:bruno-common
npm run build:bruno-requests
npm run build:bruno-filestore
npm run build:bruno-converters
npm run build:bruno-query
npm run build:graphql-docs
npm run build:schema-types
Building the renderer
npm run build:web # rsbuild production build of @usebruno/app
Packaging the desktop app
The desktop app is packaged with electron-builder. Platform-specific scripts:
npm run build:electron # default platform
npm run build:electron:mac
npm run build:electron:win
npm run build:electron:linux
npm run build:electron:deb
npm run build:electron:rpm
npm run build:electron:snap
These wrap electron-builder --config electron-builder-config.js. macOS builds are notarized (notarize.js, using APPLE_ID/APPLE_ID_PASSWORD). The Linux targets produce AppImage, .deb, .rpm, and Snap artifacts. The app is also distributed through Homebrew, Chocolatey, Scoop, winget, Flatpak, AUR, and an APT repository (debian.usebruno.com).
Note
Publishing to new package managers is documented in the repo's
publishing.md. Because Bruno is distributed across many ecosystems, a release fans out to multiple package channels.
Building and publishing the CLI
The CLI is published to npm as @usebruno/cli and as Docker images.
Docker images are built from packages/bruno-cli/docker/images/{alpine,debian}/Dockerfile. Both:
- Base on
node:22-alpine(default) ornode:22-slim(debian). - Accept a
BRUNO_VERSIONbuild arg (validated as semver) andnpm install -g @usebruno/cli@<version>. - Set
ENTRYPOINT ["bru"],WORKDIR /bruno, and run as the non-rootnodeuser (UID 1000). - Build for
linux/amd64andlinux/arm64.
Images publish to Docker Hub (usebruno/cli) and GHCR (ghcr.io/usebruno/cli) with a tag matrix: latest, latest-alpine/latest-debian, alpine/debian, exact versions (3.3.0), and floating <major>/<major.minor> tags. Unsuffixed tags resolve to the alpine variant.
docker build --build-arg BRUNO_VERSION=3.3.0 -t usebruno/cli:3.3.0-alpine ./images/alpine
12. Operating the CLI in CI/CD
The CLI is the operational surface most administrators care about. See the User Guide's CLI section for the complete flag reference; this section covers operational concerns.
Exit-code-driven gating
bru run returns a meaningful exit code (see the table in the User Guide). Code 0 means everything passed; 1 means at least one request/test/assertion failed; codes 2–13 indicate specific configuration problems; 255 is a generic error. CI should fail the job on any non-zero code — which happens automatically since bru propagates the exit status.
Reporters for CI
Emit a JUnit report for your CI's test view, and optionally JSON/HTML for archival:
bru run -r --reporter-junit results.xml --reporter-html results.html
Use --reporter-skip-body (or the finer-grained --reporter-skip-request-body/--reporter-skip-response-body/--reporter-skip-headers) to keep reports small and avoid persisting sensitive payloads into build artifacts.
Tip
--reporter-skip-headers "Authorization"strips a named header from the report. Use it (or--reporter-skip-all-headers) to keep credentials out of CI artifacts.
Sandbox in CI
Run CI in safe mode (the default). Only switch to --sandbox developer for collections your team controls and that require Node APIs. This limits the blast radius if a collection is ever modified maliciously.
Containerized runs
Mount the collection at /bruno and pass bru arguments after the image:
docker run --rm -v "$(pwd)":/bruno usebruno/cli:latest run -r --reporter-junit results.xml
Pin an exact image version (usebruno/cli:1.16.0) in production pipelines for reproducibility; use the debian variant if you hit glibc/SSL issues with alpine.
Flaky-network and TLS controls
--cache-ssl-sessionspeeds up many-request runs by reusing TLS sessions.--delay <ms>paces requests against rate-limited APIs.--cacert/--client-cert-confighandle private CAs and mTLS without disabling verification.--noproxyremoves proxy interference in locked-down build environments.
13. Development and Contributing
Repository setup
npm install # install all workspace dependencies
npm run setup # project setup script
Before the desktop app can run in development, the JS sandbox libraries must be bundled:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
Running in development
npm run dev:web # start the renderer dev server (rsbuild)
npm run dev:electron # start the Electron main process (loads the dev server)
npm run dev # orchestrated dev (scripts/dev.js)
npm run storybook # component workbench for the renderer
The Electron main process loads http://localhost:${BRUNO_DEV_PORT:-3000} in development and prints a clear error if the dev server isn't running.
Code style and hooks
- ESLint (flat config,
eslint.config.js) with@stylisticand@typescript-eslintplugins. - Husky + nano-staged run
eslint --fixon staged.js/.ts/.jsxfiles pre-commit. - Coding conventions are documented in
CODING_STANDARDS.md; contribution process incontributing.md.
Key dependency pins
The root overrides pin axios@1.13.6, rollup@3.30.0, and pbkdf2@3.1.5 across all packages — a deliberate measure to keep the network and crypto surface consistent and avoid duplicate/incompatible versions in the dependency tree.
14. Testing Strategy
Bruno tests at three levels:
| Level | Tooling | Where |
|---|---|---|
| Unit / integration | Jest (ts-jest, babel-jest) |
Co-located *.spec.js/*.test.ts across packages |
| End-to-end (app) | Playwright | playwright/ and tests/ (default, SSL, auth, system-pac projects) |
| Benchmarks | Playwright benchmark config | playwright.benchmark.config.ts, tests/benchmarks/ |
npm run test:e2e # default + system-pac projects
npm run test:e2e:ssl # SSL project
npm run test:e2e:auth # auth project
npm run test:benchmark # performance benchmarks
A dedicated test API server and sample collections live in packages/bruno-tests — an Express/GraphQL/WebSocket server (endpoints like /ping, /headers, /query, and auth routes for basic/bearer/cookie) that the CLI and E2E tests run real requests against. The repository also includes a Docker Compose setup under packages/bruno-tests for exercising the CLI in a container and producing JSON/JUnit/HTML reports.
Note
The extensive
tests/tree (auth, cookies, environments, gRPC, websockets, SSL, scripting, import, interpolation, runner, …) is Playwright E2E coverage, not application source. It exercises real user flows against the bundled test server.
15. Potential Improvements
These observations are drawn from the codebase as it stands and are offered as forward-looking guidance, not defects.
- Re-enable commented-out script APIs. Several
brumethods are commented out with TODOs noting they "work in the request lifecycle but do not update the UI" —deleteGlobalEnvVar,setCollectionVar/deleteCollectionVar/getAllCollectionVars, and global-env bulk delete. Resolving the renderer state-sync issue would let these ship, giving scripts symmetric read/write access across all variable scopes. - Performance on large collections/responses. Community reports note high memory use and slowness on large collections and big responses on some versions. The worker-thread parser (
BruParserWorker) is a good foundation; extending worker offloading to response handling and virtualizing more of the renderer would help. - Unify the auth-mode detection.
req.getAuthMode()infers the auth scheme heuristically from headers and request fields; a single authoritative auth descriptor on the request object would be more robust than inference. - Document the developer-sandbox risk in-product. Given that developer mode is not a security boundary, a clear in-app warning when enabling it for a collection (especially an imported one) would reduce the chance of running untrusted Node code unawares.
- Consolidate the two collection-config formats. Supporting both
bruno.jsonandopencollection.ymlis flexible but doubles the config surface; a documented migration path toward OpenCollection would simplify tooling over time. - Response-body size calculation. A TODO in
bruno-response.jsnotes that body-size calculation should move to a shared utility for reuse — a small refactor that would remove duplication across the app and CLI.
16. Quick Reference
Package versions
| Package | Version |
|---|---|
bruno (desktop app) |
2.0.0 |
@usebruno/app |
2.0.0 |
@usebruno/cli |
1.16.0 |
@usebruno/js |
0.12.0 |
@usebruno/lang |
0.12.0 |
@usebruno/schema |
0.7.0 |
@usebruno/requests, filestore, converters, query, common, graphql-docs, toml |
0.1.0 |
Key environment variables
| Variable | Effect |
|---|---|
DISABLE_SINGLE_INSTANCE=true |
Allow multiple app instances |
BRUNO_DEV_PORT |
Renderer dev-server port (default 3000) |
ELECTRON_USER_DATA_PATH |
Override user-data dir (dev) |
DISABLE_SAMPLE_COLLECTION_IMPORT |
Skip first-run sample import |
NODE_EXTRA_CA_CERTS |
Extra CA certs for TLS |
VAULT_ADDR / VAULT_TOKEN / VAULT_NAMESPACE |
Vault secret sourcing |
CLI exit codes
0 success · 1 failed checks · 2 missing output dir · 3 infinite loop · 4 not in a collection · 5 file not found · 6 env not found · 7/8 bad env override · 9 bad output format · 10 invalid file · 11 workspace not found · 12 global env needs workspace · 13 global env not found · 255 generic error.
Sandbox modes
| Mode | Runtime | Security |
|---|---|---|
safe (default) |
QuickJS (WASM) | Isolated sandbox |
developer |
Node.js VM | Full host access — trusted collections only |
Build commands
| Command | Builds |
|---|---|
npm run build:web |
Renderer (rsbuild) |
npm run build:electron:{mac,win,linux,deb,rpm,snap} |
Desktop packages (electron-builder) |
npm run build:bruno-* |
Individual shared libraries (Rollup) |
Docker images
usebruno/cli and ghcr.io/usebruno/cli — alpine (default, node:22-alpine) and debian (node:22-slim) variants, linux/amd64 + linux/arm64, entrypoint bru, workdir /bruno, user node.
Where things live
| Concern | Package / path |
|---|---|
.bru grammar |
packages/bruno-lang/v2/src/bruToJson.js |
| File read/write | packages/bruno-filestore/src |
| HTTP/gRPC/WS engine | packages/bruno-requests/src |
Script sandbox + bru/req/res |
packages/bruno-js/src |
| Variable interpolation | packages/bruno-common/src |
| Importers | packages/bruno-converters/src |
| Electron main / IPC | packages/bruno-electron/src |
| Renderer (React) | packages/bruno-app/src |
| CLI commands | packages/bruno-cli/src/commands |