Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Bruno Administrator & Developer Guide

Generated June 2026 from usebruno/bruno at b70bfb2

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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

  1. Executive Summary
  2. Architecture Overview
  3. The Monorepo: Package Reference
  4. The Bru Language Pipeline
  5. The Desktop App: Electron Architecture
  6. The Request Engine
  7. The Script Sandbox
  8. System Requirements
  9. Configuration Reference
  10. Security
  11. Build and Release
  12. Operating the CLI in CI/CD
  13. Development and Contributing
  14. Testing Strategy
  15. Potential Improvements
  16. 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:

  1. The file format and language (@usebruno/lang, @usebruno/filestore, @usebruno/schema) — parse and serialize .bru/.yml collections to and from an in-memory JSON model.
  2. 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.
  3. The surfaces (bruno-electron + @usebruno/app for the desktop GUI; @usebruno/cli for 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 to http), seq (defaults to 1).
  • HTTP method blocks — get, post, put, delete, patch, options, head, connect, trace, and http (custom method). Each carries url plus per-request fields.
  • grpc, ws — gRPC and WebSocket request blocks.
  • query, params:path, params:query — parameters.
  • headers, metadata — header and metadata dictionaries.
  • auths — one of auth:awsv4|basic|bearer|digest|ntlm|oauth1|oauth2|wsse|apikey, plus OAuth 2.0 additional-parameter sub-blocks (auth:oauth2:additional_params:*).
  • bodies — one of body:json|text|xml|sparql|graphql|graphql:vars|form-urlencoded|multipart-form|file|grpc|ws, or bare body.
  • 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/followRedirects to booleans, timeout/maxRedirects/keepAliveInterval to numbers; timeout may be the literal inherit).

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 (.bru and OpenCollection .yml) is why the CLI's import command takes a --collection-format flag, 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, a preload.js bridge, and webviewTag: true.
  • A minimum size of 700×400, persisted bounds and maximized state (via a window-state store).
  • Platform-specific chrome: hiddenInset title bar on macOS, hidden on 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).

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/ai and the @ai-sdk/anthropic / @ai-sdk/openai dependencies 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.

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 to 1.13.6 across the monorepo via an overrides entry).
  • gRPC — via @grpc/grpc-js and @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, and utils.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-insensitive PropertyList), onFail, setTimeout, setMaxRedirects, disableParsingResponseJson.
  • res (bruno-response.js) — the response: status/headers/body/timing, getSize, a read-only headerList, 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 .env is read at run time (in the CLI, in run.js) and merged into process.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 safe unless a specific collection genuinely needs Node APIs.

Caution

Treat a Bruno collection like source code you are about to execute. A script:pre-request or tests block 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

  • .env at the collection root keeps secrets out of the committed .bru files; reference them as {{process.env.NAME}}. The .env file 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 (including http:) 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 via shell.openExternal. setWindowOpenHandler denies 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) or node:22-slim (debian).
  • Accept a BRUNO_VERSION build arg (validated as semver) and npm install -g @usebruno/cli@<version>.
  • Set ENTRYPOINT ["bru"], WORKDIR /bruno, and run as the non-root node user (UID 1000).
  • Build for linux/amd64 and linux/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-session speeds up many-request runs by reusing TLS sessions.
  • --delay <ms> paces requests against rate-limited APIs.
  • --cacert / --client-cert-config handle private CAs and mTLS without disabling verification.
  • --noproxy removes 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 @stylistic and @typescript-eslint plugins.
  • Husky + nano-staged run eslint --fix on staged .js/.ts/.jsx files pre-commit.
  • Coding conventions are documented in CODING_STANDARDS.md; contribution process in contributing.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 bru methods 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.json and opencollection.yml is 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.js notes 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