# Bruno Administrator & Developer Guide

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](usebruno__bruno__UserGuide.md) 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](#1-executive-summary)
2. [Architecture Overview](#2-architecture-overview)
3. [The Monorepo: Package Reference](#3-the-monorepo-package-reference)
4. [The Bru Language Pipeline](#4-the-bru-language-pipeline)
5. [The Desktop App: Electron Architecture](#5-the-desktop-app-electron-architecture)
6. [The Request Engine](#6-the-request-engine)
7. [The Script Sandbox](#7-the-script-sandbox)
8. [System Requirements](#8-system-requirements)
9. [Configuration Reference](#9-configuration-reference)
10. [Security](#10-security)
11. [Build and Release](#11-build-and-release)
12. [Operating the CLI in CI/CD](#12-operating-the-cli-in-cicd)
13. [Development and Contributing](#13-development-and-contributing)
14. [Testing Strategy](#14-testing-strategy)
15. [Potential Improvements](#15-potential-improvements)
16. [Quick Reference](#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

```text
                         ┌───────────────────────────────────────────────┐
                         │                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:

```text
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`).

### 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/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.

### 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 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:

```json
{
  "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](#7-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:

```bash
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

```bash
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:

```bash
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.

```bash
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](usebruno__bruno__UserGuide.md#16-the-bruno-cli) 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:

```bash
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:

```bash
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

```bash
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:

```bash
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```

### Running in development

```bash
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/` |

```bash
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` |

---

_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. Unofficial; not affiliated with or reviewed by the project._
