# Bruno User Guide

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](#1-what-is-bruno)
2. [Quick Start](#2-quick-start)
3. [Installation](#3-installation)
4. [Core Concepts](#4-core-concepts)
5. [Creating and Sending Requests](#5-creating-and-sending-requests)
6. [Request Bodies](#6-request-bodies)
7. [Other Request Types: GraphQL, gRPC, WebSocket, SSE](#7-other-request-types-graphql-grpc-websocket-sse)
8. [Authentication](#8-authentication)
9. [Variables and Environments](#9-variables-and-environments)
10. [Scripting](#10-scripting)
11. [Assertions and Tests](#11-assertions-and-tests)
12. [Cookies](#12-cookies)
13. [Importing Collections](#13-importing-collections)
14. [Code Generation](#14-code-generation)
15. [Collaborating with Git and Workspaces](#15-collaborating-with-git-and-workspaces)
16. [The Bruno CLI](#16-the-bruno-cli)
17. [The Bru File Format](#17-the-bru-file-format)
18. [Troubleshooting](#18-troubleshooting)
19. [Frequently Asked Questions](#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](https://www.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](https://www.usebruno.com/downloads), or use a package manager (see [Installation](#3-installation)). For example, on macOS:

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

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

```text
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](https://www.usebruno.com/downloads)

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

=== "macOS"

    ```bash
    # Homebrew
    brew install bruno
    ```

=== "Windows"

    ```bash
    # Chocolatey
    choco install bruno

    # Scoop
    scoop bucket add extras
    scoop install bruno

    # winget
    winget install Bruno.Bruno
    ```

=== "Linux"

    ```bash
    # Snap
    snap install bruno

    # Flatpak
    flatpak install com.usebruno.Bruno

    # Arch Linux (AUR)
    yay -S bruno
    ```

=== "Linux (APT)"

    ```bash
    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](#16-the-bruno-cli) for installation and full usage. The short version:

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

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

```text
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](#17-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:

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

Bruno resolves `{{baseUrl}}` and `{{userId}}` from the active set of variables before sending. See [Variables and Environments](#9-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](#10-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](#11-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:

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

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

params:path {
  id: 42
}
```

### Headers

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

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

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

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

### Form URL-encoded body

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

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

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

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

```text
auth:basic {
  username: ada
  password: {{password}}
}
```

```text
auth:bearer {
  token: {{accessToken}}
}
```

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

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

```text
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](#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.

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

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

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

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

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

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

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

```bash
# 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`](#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"

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

=== "Docker"

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

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

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

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

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

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

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

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

  ```text
  headers {
    content-type: application/json
  }
  ```

- **Text blocks** — free text inside `{ }` (used for bodies, scripts, docs):

  ```text
  body:json {
    { "username": "John" }
  }
  ```

- **List blocks** — a list of items inside `[ ]`:

  ```text
  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](#8-authentication)) |
| `body:*` | The request body (see [Request Bodies](#6-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:

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

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

---

_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._
