Jesse A. Green / notebook

Bruno User 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.

Bruno is an open-source, offline-first API client for exploring, testing, and automating HTTP, GraphQL, gRPC, WebSocket, and Server-Sent Event APIs. It is a direct alternative to tools like Postman and Insomnia, with one defining difference: Bruno stores your API collections as plain-text files on your own filesystem, so you can version them with Git like the rest of your code.

This guide covers everything you need to use Bruno productively — installing the desktop app, building and sending requests, working with variables and environments, scripting and testing, importing collections from other tools, collaborating through Git, and running collections headlessly in CI/CD with the Bruno CLI.

Note

Bruno comes in two forms that share the same file format and engine: the Bruno desktop app (a graphical client for Mac, Windows, and Linux) and the Bruno CLI (bru, a command-line runner for automation). This guide documents both. Where a feature is desktop-only or CLI-only, it is called out.


Table of Contents

  1. What Is Bruno
  2. Quick Start
  3. Installation
  4. Core Concepts
  5. Creating and Sending Requests
  6. Request Bodies
  7. Other Request Types: GraphQL, gRPC, WebSocket, SSE
  8. Authentication
  9. Variables and Environments
  10. Scripting
  11. Assertions and Tests
  12. Cookies
  13. Importing Collections
  14. Code Generation
  15. Collaborating with Git and Workspaces
  16. The Bruno CLI
  17. The Bru File Format
  18. Troubleshooting
  19. Frequently Asked Questions

1. What Is Bruno

Bruno is an API client — a tool that lets you compose HTTP (and other protocol) requests, send them, inspect the responses, and organize those requests into reusable collections. Where Bruno differs from the established players is in its philosophy:

  • Offline-only. Bruno has no cloud account, no sync server, and no telemetry-gated features. Your collections never leave your machine unless you move them. The project has publicly committed to never adding cloud sync.
  • Plain-text, on disk. Each request is saved as a human-readable .bru file (or, optionally, a .yml file). A collection is just a folder of these files. There is no opaque binary workspace database.
  • Git-native collaboration. Because collections are folders of text files, you collaborate on them the same way you collaborate on source code: commit, branch, diff, review, merge. Bruno even ships a built-in visual diff viewer and Git integration.
  • A markup language called Bru. The .bru format is a small, purpose-built markup language for describing API requests. It is designed to be readable and diff-friendly.

Tip

If you have ever lost work because a SaaS API client logged you out, or struggled to share a collection with a teammate without exporting and re-importing a giant JSON file, Bruno's file-on-disk model is the feature that will matter most to you. Put your collection folder in a Git repo and your whole team works from the same source of truth.

Who Bruno is for

You are… Bruno gives you…
A developer testing your own API A fast client with scripting, environments, and tests, all version-controlled alongside your code
A QA engineer Assertions, test scripts, and a CLI that runs the same collections in CI
A team lead Git-based review of API collections — no shared cloud account required
A privacy-conscious user A tool that keeps 100% of your data on your device

Licensing

Bruno's desktop app and CLI are MIT-licensed and free. A subset of advanced features is offered through paid commercial editions (the "Golden Edition" and other paid tiers); the overwhelming majority of functionality is free and open source. See usebruno.com/pricing for the current commercial offering.


2. Quick Start

This section gets you from zero to your first successful request in a few minutes.

Step 1 — Install Bruno

Download the installer for your platform from usebruno.com/downloads, or use a package manager (see Installation). For example, on macOS:

brew install bruno

Step 2 — Create a collection

  1. Open Bruno.
  2. Click Create Collection.
  3. Give it a name (for example, My First API) and choose a folder on disk where the collection will live.

Bruno creates a folder at that location containing a small configuration file. Everything you add to the collection becomes a file inside this folder.

Note

The folder you choose is your collection. You can open it in your editor, commit it to Git, or copy it to another machine — Bruno will read it back identically.

Step 3 — Create and send a request

  1. Right-click the collection in the sidebar and choose New Request.

  2. Name it Get Posts, set the method to GET, and enter a URL:

    https://jsonplaceholder.typicode.com/posts/1
    
  3. Press Ctrl/Cmd + Enter (or click Send).

You will see the response body, status code, headers, timing, and size in the response pane.

Step 4 — Save it

Press Ctrl/Cmd + S. Bruno writes a file named Get Posts.bru into your collection folder. Open it in any text editor and you will see the request expressed in the Bru language:

meta {
  name: Get Posts
  type: http
  seq: 1
}

get {
  url: https://jsonplaceholder.typicode.com/posts/1
}

That is the whole model: a request is a file, a collection is a folder.


3. Installation

Bruno ships as a desktop application for macOS, Windows, and Linux. You can install it from the website or from your platform's package manager.

Desktop app

Download (all platforms)

Download the latest installer from the official downloads page and run it:

https://www.usebruno.com/downloads

Builds are available for macOS (Apple Silicon and Intel), Windows, and Linux (AppImage, .deb, .rpm, and Snap).

macOS

# Homebrew
brew install bruno

Windows

# Chocolatey
choco install bruno

# Scoop
scoop bucket add extras
scoop install bruno

# winget
winget install Bruno.Bruno

Linux

# Snap
snap install bruno

# Flatpak
flatpak install com.usebruno.Bruno

# Arch Linux (AUR)
yay -S bruno

Linux (APT)

sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
  | gpg --dearmor \
  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
  | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update && sudo apt install bruno

Tip

On Linux, installing via APT (the steps above) means apt upgrade keeps Bruno up to date along with the rest of your system packages.

Verifying the desktop install

Launch Bruno from your applications menu. The window title reads Bruno. Open Help → About (or the application menu) to confirm the installed version.

The Bruno CLI

The CLI is a separate, smaller package distributed through npm and Docker. See The Bruno CLI for installation and full usage. The short version:

npm install -g @usebruno/cli
bru --version
1.16.0

Important

The desktop app and the CLI are versioned independently. The desktop app and CLI both read and run the same .bru collections, so a collection authored in the app runs unchanged under the CLI.


4. Core Concepts

A handful of concepts underpin everything in Bruno. Understanding them makes the rest of the tool obvious.

Collections are folders

A collection is a folder on your filesystem. At its root sits a configuration file — bruno.json (the classic format) or opencollection.yml (the OpenCollection format). Subfolders inside the collection become folders in Bruno's sidebar, letting you organize requests hierarchically.

My API/
├── bruno.json              ← collection configuration
├── environments/
│   ├── Local.bru           ← an environment
│   └── Production.bru
├── Auth/
│   └── Login.bru           ← a request inside the "Auth" folder
└── Get Posts.bru           ← a request at the collection root

Note

Because the collection is a normal folder, you can move it, copy it, zip it, or — most importantly — commit it to Git. There is no hidden database.

Requests are .bru files

Each request is a single .bru file. The file records the method, URL, query parameters, headers, body, authentication, variables, scripts, assertions, tests, and documentation for that one request. The format is described in full in The Bru File Format.

Environments hold variables

An environment is a named set of variables, stored in the collection's environments/ folder. Typical use: a Local environment pointing baseUrl at http://localhost:3000, and a Production environment pointing it at https://api.example.com. You switch the active environment from a dropdown; your requests reference {{baseUrl}} and resolve to whichever environment is selected.

Variables interpolate with {{ }}

Anywhere a value is expected — a URL, a header, a body, an auth field — you can interpolate a variable with double curly braces:

get {
  url: {{baseUrl}}/users/{{userId}}
}

Bruno resolves {{baseUrl}} and {{userId}} from the active set of variables before sending. See Variables and Environments for the full list of variable scopes and how they layer.

Scripts run around requests

Each request can carry a pre-request script (runs before the request is sent) and a post-response script (runs after the response arrives). Scripts are JavaScript and have access to a bru object for reading and writing variables, a req object for inspecting and mutating the outgoing request, and a res object for the response. See Scripting.

Tests and assertions verify responses

Assertions are declarative checks (for example, res.status equals 200). Tests are JavaScript test(...) blocks using a Chai-style expect API. Both run after the response and report pass/fail. See Assertions and Tests.


5. Creating and Sending Requests

Anatomy of a request

A Bruno request consists of:

  • A method — GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, CONNECT, TRACE, or a custom method.
  • A URL — including any path parameters (/users/:id).
  • Query parameters — appended to the URL as ?key=value.
  • Headers — request headers.
  • A body — for methods that send one.
  • Authentication — an auth scheme applied to the request.

Creating a request

  1. Right-click a collection or folder in the sidebar → New Request.
  2. Enter a name, pick the method, and type the URL.
  3. Configure parameters, headers, body, and auth in the tabbed request pane.
  4. Send with Ctrl/Cmd + Enter, then save with Ctrl/Cmd + S.

Query parameters

Add query parameters in the Query tab, or type them directly into the URL — Bruno keeps the two in sync. In the .bru file they appear as a params:query block:

get {
  url: {{baseUrl}}/search?q=bruno&limit=10
}

params:query {
  q: bruno
  limit: 10
}

Path parameters

Use a :name segment in the URL to declare a path parameter, then provide its value. In the file this is a params:path block:

get {
  url: {{baseUrl}}/users/:id
}

params:path {
  id: 42
}

Headers

Add headers in the Headers tab. They are stored in a headers block:

headers {
  Content-Type: application/json
  Accept: application/json
}

Tip

Any row in a parameters, headers, or body table can be disabled without deleting it. In the file, a disabled entry is prefixed with a tilde (~), for example ~X-Debug: true. This is handy for keeping optional headers around without sending them.

Sending and reading the response

After sending, the response pane shows:

  • Status — code and text (for example 200 OK).
  • Time — how long the request took.
  • Size — header and body byte sizes.
  • Body — pretty-printed JSON/XML/HTML, or raw text, with a built-in viewer.
  • Headers — the response headers.
  • Timeline — a low-level view of the request/response exchange.

Note

Bruno automatically detects and pretty-prints common content types. If a response is misdetected, you can switch the response viewer's format manually.


6. Request Bodies

Bruno supports a full range of request body types. Select the body type in the Body tab of the request pane. Each maps to a specific block in the .bru file.

Body type Bru block Use it for
JSON body:json REST APIs that accept JSON
Text body:text Plain text payloads
XML body:xml SOAP / XML APIs
SPARQL body:sparql SPARQL query endpoints
GraphQL body:graphql GraphQL queries (with a companion body:graphql:vars for variables)
Form URL-encoded body:form-urlencoded application/x-www-form-urlencoded forms
Multipart form body:multipart-form File uploads and mixed form data
File / binary body:file Sending a raw file as the body

JSON body

post {
  url: {{baseUrl}}/users
  body: json
}

body:json {
  {
    "name": "Ada Lovelace",
    "email": "[email removed]"
  }
}

Form URL-encoded body

post {
  url: {{baseUrl}}/login
  body: form-urlencoded
}

body:form-urlencoded {
  username: ada
  password: {{secretPassword}}
}

Multipart form (file uploads)

In a multipart body, a field whose value uses the @file(...) reference uploads a file from disk. You can also pin a content type to a field with @contentType(...):

post {
  url: {{baseUrl}}/upload
  body: multipart-form
}

body:multipart-form {
  caption: My holiday photo
  avatar: @file(/Users/ada/pictures/me.png)
}

Tip

The @file(...) reference stores only the path to the file in the .bru, not the file's contents — so your collection stays small and diff-friendly even when requests upload large files. The referenced file must exist on the machine running the request.

File / binary body

post {
  url: {{baseUrl}}/raw-upload
  body: file
}

body:file {
  file: @file(/path/to/data.bin) @contentType(application/octet-stream)
}

7. Other Request Types: GraphQL, gRPC, WebSocket, SSE

Bruno is not limited to plain HTTP. It is a multi-protocol client.

GraphQL

Create a GraphQL request and write your query in the GraphQL body editor; provide variables in the variables panel. Bruno ships a GraphQL docs explorer so you can browse a schema's types and fields inside the app.

post {
  url: {{baseUrl}}/graphql
  body: graphql
}

body:graphql {
  query GetUser($id: ID!) {
    user(id: $id) { id name email }
  }
}

body:graphql:vars {
  {
    "id": "42"
  }
}

gRPC

Bruno can call gRPC services. Point it at a service, load the .proto definitions (or use server reflection), pick a method, and send a message. gRPC requests are stored with a grpc block and body:grpc message blocks.

Note

gRPC support includes unary and streaming method types and a method browser populated from your proto files or via reflection.

WebSocket

Open a WebSocket connection, send messages, and watch incoming frames in real time. WebSocket requests use a ws block with body:ws message blocks.

Server-Sent Events (SSE)

Bruno can subscribe to an SSE endpoint and display the streamed events as they arrive, which is useful for testing event streams and LLM token streams.

Tip

For long-lived protocols (WebSocket, SSE, gRPC streaming) the response pane updates continuously as data arrives, rather than showing a single static response.


8. Authentication

Bruno supports authentication at the request, folder, and collection levels. Lower levels can inherit auth from their parents, so you can configure auth once on a collection and have every request use it.

Supported auth schemes

Scheme Bru block Notes
Basic auth:basic Username + password
Bearer auth:bearer A bearer token
API Key auth:apikey Key, value, and placement (header or query)
OAuth 2.0 auth:oauth2 Four grant types — see below
OAuth 1.0 auth:oauth1 Full OAuth 1.0a signing
Digest auth:digest HTTP Digest auth
AWS Signature v4 auth:awsv4 Access key, secret, session token, service, region, profile
NTLM auth:ntlm Username, password, domain
WSSE auth:wsse WS-Security username token

Basic, Bearer, and API Key

auth:basic {
  username: ada
  password: {{password}}
}
auth:bearer {
  token: {{accessToken}}
}
auth:apikey {
  key: X-API-Key
  value: {{apiKey}}
  placement: header
}

For API key auth, placement is either header (sent as a header) or queryparams (appended to the URL).

OAuth 2.0

Bruno implements four OAuth 2.0 grant types:

Grant type grant_type value Typical use
Authorization Code authorization_code Web apps with user login (supports PKCE)
Client Credentials client_credentials Machine-to-machine
Password password Resource owner password (legacy)
Implicit implicit Browser-based (legacy)

Bruno can automatically fetch and automatically refresh tokens (auto_fetch_token, auto_refresh_token), supports PKCE for the authorization code flow, and lets you control where credentials are placed (request body vs. Basic header) and where the resulting token is placed (header with a configurable prefix, or query parameter). Fetched tokens are cached under a credential ID so multiple requests can share them.

auth:oauth2 {
  grant_type: authorization_code
  callback_url: {{callbackUrl}}
  authorization_url: {{authUrl}}
  access_token_url: {{tokenUrl}}
  client_id: {{clientId}}
  client_secret: {{clientSecret}}
  scope: openid profile
  pkce: true
  token_placement: header
  token_header_prefix: Bearer
  auto_refresh_token: true
}

Tip

In scripts you can read OAuth 2.0 credential variables with bru.getOauth2CredentialVar(name) and force a re-fetch by clearing a cached credential with bru.resetOauth2Credential(credentialId).

AWS Signature v4

auth:awsv4 {
  accessKeyId: {{awsAccessKey}}
  secretAccessKey: {{awsSecret}}
  service: execute-api
  region: us-east-1
}

Bruno can also resolve AWS credentials from a named profile (profileName) and supports session tokens.

Important

Never commit real secrets in plain text. Use environment variables ({{apiKey}}) and keep secret values out of the .bru files you check into Git — see Secrets and the .env file.


9. Variables and Environments

Variables are how you keep requests reusable across different servers, users, and runs. Bruno has several variable scopes, and they layer in a defined order of precedence.

Variable scopes

Scope Set where Lifetime Settable from scripts
Global environment Workspace-level global environments Across collections bru.setGlobalEnvVar()
Collection Collection settings The collection Read-only from scripts
Environment The active environment Until you switch environments bru.setEnvVar() (optionally persisted)
Folder Folder settings Requests in that folder Read-only from scripts
Request Request's Vars tab One request Read-only from scripts
Runtime Scripts at run time The current run bru.setVar()
Process env OS / .env file The process Read-only (bru.getProcessEnv())

Note

When you reference {{name}}, Bruno merges all scopes into one lookup. If the same name exists in several scopes, the higher-precedence scope wins. The effective precedence (lowest to highest) is: global environment → collection → environment → folder → request → OAuth2 credentials → runtime → prompt variables, with process.env available under {{process.env.NAME}}.

Environments

Create environments from the environment selector (top-right). Each environment is a .bru file under environments/. Switching the active environment changes which values {{...}} resolves to.

vars {
  baseUrl: https://api.staging.example.com
  apiKey: {{process.env.STAGING_API_KEY}}
}

Tip

Reference secrets indirectly. Instead of writing your real API key into an environment file, point the environment variable at a process environment variable ({{process.env.STAGING_API_KEY}}) and supply the actual value through your shell or a .env file that you do not commit.

Pre-request and post-response variables

A request can declare variables that are computed before it runs (vars:pre-request) or extracted from the response after it runs (vars:post-response). Post-response vars are the standard way to capture a token from a login response and reuse it in later requests.

vars:post-response {
  accessToken: res.body.token
}

A variable name prefixed with @ is local — it is available within the run but not persisted to the environment.

Secrets and the .env file

If a .env file exists at the root of a collection, Bruno (and the CLI) loads it and exposes its keys under process.env. This is the recommended way to keep secrets out of version control:

  1. Add real secret values to .env at the collection root.
  2. Add .env to your .gitignore.
  3. Reference them as {{process.env.MY_SECRET}} in requests and environments.

Caution

The .env file is read in plain text. Treat it like any other secret file: never commit it, and restrict its filesystem permissions.

Secret variables and encryption (desktop)

The desktop app can store certain sensitive values (such as secret environment variables) encrypted at rest using your operating system's secure storage. This keeps secrets out of the plain-text collection files while still letting you use them in the app.


10. Scripting

Scripts let you run JavaScript before a request is sent and after its response arrives. Use them to build dynamic payloads, set headers, capture tokens, chain requests, and more.

Pre-request and post-response scripts

script:pre-request {
  // Runs before the request is sent
  bru.setVar("requestId", crypto.randomUUID());
  req.setHeader("X-Request-Id", bru.getVar("requestId"));
}

script:post-response {
  // Runs after the response arrives
  if (res.status === 200) {
    bru.setEnvVar("accessToken", res.body.token);
  }
}

The bru object

The bru object is your handle on the run. Key methods:

Variables

Method Purpose
bru.getVar(key) / bru.setVar(key, value) Runtime variables
bru.getEnvVar(key) / bru.setEnvVar(key, value, {persist}) Environment variables (set persist: true to write back to the environment file; persisted values must be strings)
bru.getGlobalEnvVar(key) / bru.setGlobalEnvVar(key, value) Global environment variables
bru.getCollectionVar(key) Read a collection variable
bru.getFolderVar(key) / bru.getRequestVar(key) Read folder / request variables
bru.getProcessEnv(key) Read a process.env value
bru.hasVar(key) / bru.deleteVar(key) / bru.getAllVars() Inspect and manage runtime vars
bru.getOauth2CredentialVar(key) / bru.resetOauth2Credential(id) OAuth 2.0 credentials

Flow control and utilities

Method Purpose
bru.setNextRequest(name) Jump to a named request next (collection runs)
bru.runner.skipRequest() Skip the current request
bru.runner.stopExecution() Stop the whole run
bru.runRequest(path) Run another request from within a script
bru.sendRequest(options) Send an ad-hoc HTTP request from a script
bru.sleep(ms) Await a delay
bru.getEnvName() The active environment's name
bru.getCollectionName() The collection's name
bru.cwd() The collection's path on disk
bru.isSafeMode() true if running in the safe (sandboxed) runtime
bru.interpolate(str) Interpolate {{...}} against the current variables
bru.utils.minifyJson(x) / bru.utils.minifyXml(x) Minify helpers

Note

Variable names must contain only alphanumerics plus -, _, and .. Setting a variable with an empty name, or persisting a non-string environment variable, throws an error.

The req object (pre-request)

In a pre-request script, req represents the outgoing request and lets you read and mutate it before it is sent:

  • req.url, req.method, req.headers, req.body, req.timeout, req.name, req.tags
  • req.getUrl() / req.setUrl(url), req.getMethod() / req.setMethod(m)
  • req.getHeader(name) / req.setHeader(name, value) / req.deleteHeader(name)
  • req.getBody({raw}) / req.setBody(data, {raw})
  • req.headerList — a rich, case-insensitive header collection API (get, add, upsert, remove, toObject, …)
  • req.getAuthMode() — which auth scheme is active
  • req.setTimeout(ms), req.setMaxRedirects(n)
  • req.onFail(callback) — register a handler that runs if the request fails
  • req.disableParsingResponseJson() — keep the response body as a raw string

The res object (post-response)

In a post-response script (and in tests), res represents the response:

  • res.status, res.statusText, res.headers, res.body, res.responseTime
  • res.getStatus(), res.getHeader(name), res.getBody(), res.getResponseTime(), res.getSize()
  • res.setBody(data) — rewrite the body (useful for downstream tests)
  • res.headerList — a read-only header collection API
  • res is callable: res('users[0].name') runs a query against the JSON body and returns the matched value, a concise alternative to res.body.users[0].name.

Inbuilt libraries

Scripts can require a curated set of bundled libraries without installing anything, including:

Library Use
axios Make HTTP requests from scripts
chai Assertions (expect, assert)
crypto-js Hashing and crypto
jsonwebtoken / jose Create and verify JWTs
uuid, nanoid Generate IDs
lodash Utilities
moment Dates and times
cheerio Parse and query HTML
xml2js Parse XML
ajv, tv4 JSON Schema validation
@faker-js/faker Generate fake test data
btoa / atob Base64 encode/decode

Tip

A collection can also declare its own npm dependencies (in a package.json at the collection root) for additional libraries you want available in scripts.

Sandbox modes: safe vs. developer

Because scripts are arbitrary JavaScript, Bruno runs them in one of two sandboxes:

Mode Runtime What it allows
Safe (default) QuickJS (a WebAssembly JS engine) A restricted, isolated environment — no direct access to the host filesystem or Node internals
Developer Node.js VM Full Node.js capabilities for power users who trust their collection's scripts

bru.isSafeMode() returns true in safe mode. In the CLI you choose the mode with --sandbox safe (default) or --sandbox developer.

Warning

Developer mode runs scripts with full Node.js access. Only enable it for collections whose scripts you trust. Running an untrusted collection in developer mode is equivalent to running an untrusted program. Safe mode is the default precisely to protect you from malicious or buggy scripts in shared collections.


11. Assertions and Tests

Bruno offers two complementary ways to verify a response.

Assertions (declarative)

Assertions live in the Assert tab and are simple expression → operator/value checks. They are stored in an assert block:

assert {
  res.status: eq 200
  res.body.id: isNumber
  res.headers.content-type: contains application/json
}

Each assertion targets a value (like res.status or a JSON path into res.body) and checks it against an expected condition. Disabled assertions are prefixed with ~.

Tests (scripted)

Tests are JavaScript blocks in the Tests tab, using a Chai-style expect/assert API. They give you full control and can contain arbitrary logic:

tests {
  test("status is 200", function () {
    expect(res.status).to.equal(200);
  });

  test("response has an id", function () {
    expect(res.body).to.have.property("id");
  });

  test("responds quickly", function () {
    expect(res.responseTime).to.be.below(500);
  });
}

Test results (passed / failed) appear in the response pane in the app and in the run summary in the CLI.

Tip

You can write tests that also run inside pre-request and post-response scripts. The CLI's --tests-only flag runs only requests that actually contain a test or an active assertion, which is handy for fast verification runs.


12. Cookies

Bruno maintains a cookie jar. By default, cookies returned by responses are stored and sent automatically on subsequent requests to matching domains.

Managing cookies in the app

The desktop app has a cookie manager where you can view, add, edit, and delete cookies per domain.

Cookies in scripts

The bru.cookies API lets scripts read and write cookies. Scoped to the current request URL:

script:post-response {
  // Read a cookie set by the response
  const sid = bru.cookies.get("sid");

  // Add or replace a cookie for the current URL
  await bru.cookies.add({ key: "lang", value: "en" });

  // Remove a cookie
  await bru.cookies.remove("sid");
}

For cross-URL operations, use the jar handle, which interpolates {{...}} in URLs:

script:pre-request {
  const jar = bru.cookies.jar();
  const token = await jar.getCookie("{{authBaseUrl}}/login", "access_token");
  await jar.setCookie("{{apiBaseUrl}}", { key: "sid", value: "abc123", secure: true });
}

The cookie list API also supports all(), count(), has(name), find(fn), filter(fn), toObject(), toString(), and clear().

Note

In the CLI, automatic cookie handling can be turned off for a run with --disable-cookies.


13. Importing Collections

Migrating from another tool, or have an API spec? Bruno can import from several sources.

In the desktop app

Use Import Collection to bring in:

  • Postman collections
  • Insomnia exports
  • OpenAPI / Swagger specifications
  • WSDL (SOAP) definitions
  • Other Bruno collections

The import converts the source into native Bruno requests and writes them as .bru files.

From the CLI

The CLI imports OpenAPI and WSDL sources directly:

# Import an OpenAPI spec into a new Bruno collection
bru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"

# Import from a remote spec
bru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API"

# Import a WSDL (SOAP) service
bru import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name "SOAP Service"

See bru import for all flags, including grouping (--group-by tags|path) and output format (--collection-format bru|opencollection).

Tip

When importing an OpenAPI spec, --group-by tags organizes requests by the spec's tags, while --group-by path mirrors the URL path hierarchy. Pick whichever matches how you think about the API.


14. Code Generation

Bruno can generate ready-to-use client code for any request — curl commands, language-specific HTTP snippets, and more — from the request's current configuration. Open a request's code generation panel, pick a target (language and library), and copy the generated snippet.

This is useful for:

  • Sharing a reproducible curl command in a bug report.
  • Bootstrapping client code in your application's language.
  • Documenting exactly how to call an endpoint.

Note

Generated snippets reflect the request as configured, including headers, body, and (where applicable) auth. Variables may be emitted as literal placeholders, so review the snippet before sharing it if it contains secrets.


15. Collaborating with Git and Workspaces

Bruno's headline collaboration model is Git. Because a collection is a folder of text files, collaboration is just version control.

The Git workflow

  1. Put your collection folder inside a Git repository (or git init it directly).
  2. Commit the .bru files.
  3. Push, branch, open pull requests, and review changes exactly as you would with code.
  4. Teammates clone the repo and open the folder in Bruno.

Because .bru is plain text, diffs are meaningful: a reviewer can see precisely which header changed or which assertion was added.

Tip

Add a .gitignore to your collection that excludes .env (secrets) and any local-only files. Commit the requests, environments (without secret values), and collection config.

Visual diff and built-in Git

The desktop app includes a visual diff viewer for comparing request changes and built-in Git integration for common operations, so you can review and manage changes without leaving Bruno.

Workspaces and global environments

A workspace groups multiple collections and can hold global environments that apply across all collections in the workspace. A workspace is defined by a workspace.yml file. Global environments live in the workspace's environments/ folder and are selected the same way as collection environments.

Note

Global environments require the collection to be part of a workspace. In the CLI, you select one with --global-env <name> and, if needed, point at the workspace with --workspace-path.


16. The Bruno CLI

The Bruno CLI (bru) runs your collections from the command line — perfect for CI/CD pipelines, scheduled jobs, and scripted automation. It runs the exact same .bru collections as the desktop app.

Installing the CLI

npm

npm install -g @usebruno/cli
bru --version

Docker

docker pull usebruno/cli:latest
docker run --rm usebruno/cli --version

The CLI's two commands are run and import.

bru run

Run one or more requests or folders. Execute it from inside your collection directory (the directory containing bruno.json / opencollection.yml).

# Run a single request
bru run request.bru

# Run every request in a folder
bru run folder

# Run a folder recursively
bru run folder -r

# Run the entire collection (no path = current collection, recursive)
bru run

# Run with a named environment
bru run request.bru --env local

# Override a single variable (repeatable)
bru run request.bru --env local --env-var secret=xxx

bru run options

Option Description
-r Run recursively through subfolders
--env <name> Use a named environment from environments/
--env-file <path> Use an environment from a specific .bru or .json file
--global-env <name> Use a workspace global environment
--workspace-path <path> Path to the workspace (auto-detected if omitted)
--env-var <name=value> Override one variable; may be passed multiple times
--sandbox <safe|developer> JavaScript sandbox to use (default safe)
-o, --output <path> Write results to a file
-f, --format <json|junit|html> Format of the results file (default json)
--reporter-json <path> Write a JSON report
--reporter-junit <path> Write a JUnit XML report
--reporter-html <path> Write an HTML report
--reporter-skip-all-headers Omit all headers from the report
--reporter-skip-headers <h…> Omit specific headers from the report
--reporter-skip-request-body Omit request bodies from the report
--reporter-skip-response-body Omit response bodies from the report
--reporter-skip-body Omit both request and response bodies
--tests-only Only run requests that have a test or active assertion
--bail Stop the run after the first failure
--delay <ms> Delay between requests (milliseconds)
--tags <a,b> Only run requests with these tags
--exclude-tags <a,b> Skip requests with these tags
--insecure Allow insecure (unverified TLS) server connections
--cacert <path> Custom CA certificate to verify the peer
--ignore-truststore Use only the --cacert file, ignoring the default truststore
--client-cert-config <path> Path to a client certificate config (JSON)
--disable-cookies Do not store or send cookies automatically
--noproxy Disable all proxy settings (collection and system)
--cache-ssl-session Reuse TLS sessions across requests for faster handshakes
--verbose Verbose output for debugging

Reporters

Bruno can emit machine- and human-readable reports in three formats:

# JSON report
bru run --reporter-json results.json

# JUnit XML (for CI test reporters)
bru run --reporter-junit results.xml

# HTML report (open in a browser)
bru run --reporter-html results.html

# All three at once
bru run --reporter-json results.json --reporter-junit results.xml --reporter-html results.html

The legacy --output + --format pair still works (--output results.xml --format junit).

Tip

JUnit XML is the format most CI systems understand for test reporting. Pair it with your CI's test-report step (GitHub Actions, GitLab CI, Jenkins) to see Bruno results rendered as test cases.

Exit codes

bru run exits non-zero when something fails, which lets CI fail the build automatically:

Code Meaning
0 Success
1 One or more requests, tests, or assertions failed
2 The specified output directory does not exist
3 A request chain caused an infinite loop
4 bru was run outside a collection root
5 A specified file was not found
6 The specified environment was not found
7 Environment override not provided as a string/object
8 Environment override format incorrect
9 Invalid output format requested
10 Invalid file format
11 The specified workspace was not found
12 A global environment was requested without a workspace
13 The specified global environment was not found
255 Generic / unexpected error

bru import

Import an OpenAPI or WSDL source into a Bruno collection.

bru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"
Option Alias Description
--source -s Path or URL to the source file (required)
--output -o Output directory (conflicts with --output-file)
--output-file -f Output JSON file (conflicts with --output)
--collection-name -n Name for the imported collection
--collection-format bru or opencollection (default opencollection)
--group-by -g Group OpenAPI requests by tags or path (default tags)
--insecure Skip TLS verification when fetching from URLs

The positional <type> argument is openapi or wsdl.

Running the CLI in CI/CD

GitHub Actions

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Bruno collection
        run: |
          docker run --rm \
            -v ${{ github.workspace }}:/bruno \
            usebruno/cli:latest run --output results.xml --format junit
      - name: Publish Test Report
        uses: dorny/test-reporter@v3
        if: success() || failure()
        with:
          name: Bruno Test Results
          path: ${{ github.workspace }}/results.xml
          reporter: java-junit

GitLab CI

api-tests:
  image: usebruno/cli:latest
  script:
    - bru run --output results.xml --format junit
  artifacts:
    reports:
      junit: results.xml

Running the CLI with Docker

Official images are published to Docker Hub (usebruno/cli) and GHCR (ghcr.io/usebruno/cli). The image's entrypoint is bru and its working directory is /bruno, so you mount your collection there and pass bru arguments after the image name.

# Run every request in the current collection directory
docker run -v $(pwd):/bruno usebruno/cli run

# Run a specific subfolder
docker run -v $(pwd):/bruno usebruno/cli run ./api-tests

# Run a single request and write a JUnit report
docker run --rm -v $(pwd):/bruno usebruno/cli run --reporter-junit results.xml

Two image variants are available:

Variant Base image When to use
Alpine (default) node:22-alpine The default — smallest and fastest to pull (use this unless you have a reason not to)
Debian node:22-slim If you hit SSL/glibc compatibility issues
docker pull usebruno/cli:latest          # alpine, newest
docker pull usebruno/cli:3.3.0           # exact version, immutable
docker pull usebruno/cli:debian          # debian variant

Note

On Windows native shells, replace $(pwd) with ${PWD} (PowerShell) or %cd% (CMD). Append --rm to any docker run to auto-delete the container after the run finishes — handy for CI.


17. The Bru File Format

The .bru format is Bruno's plain-text markup for requests. You rarely need to write it by hand — the app generates it — but understanding it makes diffs readable and lets you hand-edit when convenient.

Block structure

A .bru file is a series of blocks. There are three kinds:

  • Dictionary blocks — key/value pairs inside { }:

    headers {
      content-type: application/json
    }
    
  • Text blocks — free text inside { } (used for bodies, scripts, docs):

    body:json {
      { "username": "John" }
    }
    
  • List blocks — a list of items inside [ ]:

    tags [
      regression
      smoke-test
    ]
    

Common blocks

Block Contents
meta Request name, type (http/grpc/ws), and seq (display order)
get / post / put / delete / patch / options / head / connect / trace / http The HTTP method block, carrying the url and request settings
headers Request headers
params:query / params:path Query and path parameters
auth:* Authentication (see Authentication)
body:* The request body (see Request Bodies)
vars:pre-request / vars:post-response Variables computed before / extracted after
assert Declarative assertions
script:pre-request / script:post-response JavaScript scripts
tests Scripted tests
docs Markdown documentation for the request
settings Per-request settings (see below)

Conventions

  • Disabling an entry: prefix a key with ~ (for example ~X-Debug: true) to keep it in the file but exclude it from the request.
  • Local variables: prefix a variable name with @ in a vars block to make it local (not persisted).
  • File references: use @file(/path/to/file) for file bodies and multipart file fields.
  • Content type annotations: append @contentType(type) to pin a content type on a multipart or file field.
  • Multiline values: wrap a value in triple single quotes ('''…''') for multi-line content.

Per-request settings

The settings block controls request behavior:

settings {
  encodeUrl: true
  timeout: 30000
  followRedirects: true
  maxRedirects: 5
}
Setting Meaning
encodeUrl Whether to URL-encode the request URL
timeout Request timeout in milliseconds (0 = no timeout; inherit = use the parent's)
followRedirects Whether to follow HTTP redirects
maxRedirects Maximum number of redirects to follow
keepAliveInterval Keep-alive interval (where applicable)

A complete example

meta {
  name: Create User
  type: http
  seq: 3
}

post {
  url: {{baseUrl}}/users
  body: json
  auth: bearer
}

headers {
  Accept: application/json
}

auth:bearer {
  token: {{accessToken}}
}

body:json {
  {
    "name": "Ada Lovelace",
    "email": "[email removed]"
  }
}

vars:post-response {
  newUserId: res.body.id
}

assert {
  res.status: eq 201
}

tests {
  test("returns the created user id", function () {
    expect(res.body).to.have.property("id");
  });
}

docs {
  Creates a new user. Requires a valid bearer token.
}

18. Troubleshooting

Each entry follows the pattern Symptom → Cause → Solution → Prevention.

The app shows a blank screen on launch

  • Cause: The renderer failed to load (most common in development builds where the dev server isn't running; in production builds this indicates a bug).
  • Solution: Restart Bruno. If you are running from source, start the web dev server first. If it persists with an official build, report it with logs at the project's issue tracker.
  • Prevention: Use official release builds from the downloads page.

Error: The collection is not valid (neither bruno.json nor opencollection.yml found)

  • Cause: You opened (or pointed the CLI at) a folder that is not a Bruno collection root.
  • Solution: Open the folder that contains bruno.json or opencollection.yml. For the CLI, cd into the collection root before running bru run.
  • Prevention: Run bru run from the collection directory; open the collection's actual root folder in the app.

Error: Environment file not found: environments/<name>.bru

  • Cause: The --env name you passed does not match an environment file in environments/.
  • Solution: Check the exact file name under environments/ and pass that name (without extension) to --env.
  • Prevention: List your environments before a run; keep environment names consistent.

CLI exits with code 1 but requests "look fine"

  • Cause: A test or assertion failed, or a request errored. Exit code 1 means one or more requests, tests, or assertions failed.
  • Solution: Read the execution summary table the CLI prints, and inspect a generated report (--reporter-html results.html) to see which check failed.
  • Prevention: Run locally with a reporter before pushing to CI.

SSL Certificate error: <code>. Try using --insecure if you trust this source.

  • Cause: The server presented a certificate Bruno could not verify (self-signed, expired, or wrong hostname).
  • Solution: If you trust the source, add --insecure (CLI) or the equivalent app setting. For a private CA, supply the CA with --cacert <path> instead of disabling verification.
  • Prevention: Install your organization's CA into the system truststore, or reference it with --cacert.

Workspace not found / Global environment requires a workspace

  • Cause: You used --global-env but the collection isn't inside a workspace, or the workspace path couldn't be detected.
  • Solution: Ensure a workspace.yml exists in a parent directory, or pass --workspace-path explicitly.
  • Prevention: Keep collections that share global environments inside a workspace directory.

Variables in {{...}} aren't being substituted

  • Cause: The variable isn't defined in any active scope, the environment isn't selected, or the name has a typo.
  • Solution: Confirm the active environment, check the variable exists in some scope, and verify the exact name (names allow only letters, numbers, -, _, .).
  • Prevention: Define shared values at the collection or environment level and reference them consistently.

Variable name: "<name>" contains invalid characters!

  • Cause: A script tried to set a variable whose name contains characters other than alphanumerics, -, _, or ..
  • Solution: Rename the variable to use only allowed characters.
  • Prevention: Stick to simple identifier-style names.

Persistent environment variables must be strings

  • Cause: You called bru.setEnvVar(key, value, { persist: true }) with a non-string value.
  • Solution: Convert the value to a string (for example JSON.stringify(value)) before persisting, or omit persist to keep it in-memory.
  • Prevention: Only persist string values; serialize structured data yourself.

A script works in the app but fails in the CLI (or vice versa)

  • Cause: Different sandbox modes. The default is safe (QuickJS); some Node-specific APIs are unavailable there.
  • Solution: Either rewrite the script to stay within the safe sandbox, or run with --sandbox developer if you trust the collection.
  • Prevention: Prefer the inbuilt libraries and the bru/req/res APIs over raw Node APIs so scripts run identically everywhere.

Output directory <dir> does not exist

  • Cause: A reporter path points into a directory that hasn't been created (CLI exit code 2).
  • Solution: Create the output directory first, or point the reporter at an existing one.
  • Prevention: mkdir -p the reports directory in your CI step before running bru.

File upload fails / file not found

  • Cause: A @file(...) reference points to a path that doesn't exist on the machine running the request.
  • Solution: Verify the file path is correct and present on the runner (in Docker/CI, ensure the file is inside the mounted volume).
  • Prevention: Use paths relative to the collection where possible, and bind-mount the right directory in containers.

High memory use on very large collections / responses

  • Cause: Large responses or big collection runs consume memory; this has been reported on some versions.
  • Solution: Use the reporter --reporter-skip-body / --reporter-skip-response-body flags to avoid retaining large bodies, run in smaller batches, and keep Bruno updated.
  • Prevention: Split very large collections and avoid storing huge responses in reports when you don't need them.

19. Frequently Asked Questions

Q: Does Bruno require an account or internet connection? No. Bruno is offline-only. There is no account and no cloud sync, and the project has stated it will not add cloud sync.

Q: Where are my collections stored? In a folder you choose on your own filesystem. Each request is a .bru file; the collection is the folder.

Q: How do I share a collection with my team? Put the collection folder in a Git repository and have teammates clone it and open it in Bruno. That's the intended collaboration model.

Q: Is Bruno free? Yes — the desktop app and CLI are MIT-licensed and free. Some advanced features are available through paid commercial editions.

Q: Can I run my collections in CI? Yes. The Bruno CLI (bru run) runs the same collections headlessly and exits non-zero on failure, with JSON, JUnit, and HTML reporters. Official Docker images make this easy.

Q: What protocols does Bruno support? HTTP/HTTPS, GraphQL, gRPC, WebSocket, and Server-Sent Events.

Q: What auth schemes are supported? Basic, Bearer, API Key, OAuth 2.0 (four grant types, with PKCE and auto-refresh), OAuth 1.0, Digest, AWS Signature v4, NTLM, and WSSE.

Q: How do I keep secrets out of Git? Reference secrets via {{process.env.NAME}} and supply the actual values through a .env file at the collection root that you exclude from version control. The desktop app can also store secret values using your OS's secure storage.

Q: Can I import my Postman / Insomnia / OpenAPI collections? Yes. The app imports Postman, Insomnia, OpenAPI/Swagger, and WSDL. The CLI imports OpenAPI and WSDL.

Q: What's the difference between safe and developer sandbox modes? Safe mode (default) runs scripts in an isolated QuickJS engine with no host access. Developer mode runs them in a Node.js VM with full capabilities. Only use developer mode for collections you trust.

Q: Can scripts run other requests? Yes — bru.runRequest(path) runs another request, bru.setNextRequest(name) controls chaining in collection runs, and bru.sendRequest(options) sends an ad-hoc request.

Q: What's the .bru vs .yml difference? Both represent collections. .bru is Bruno's native markup; .yml is the OpenCollection YAML format. Bruno reads and writes both.

Q: How do I generate a curl command for a request? Use Bruno's code generation feature to produce curl or language-specific snippets from any request.

Q: Does Bruno collect telemetry? Bruno is privacy-focused and offline-first. Optional product analytics may be present in the desktop app; the network policy restricts outbound connections accordingly. No collection data is sent anywhere.