# Bruno: .bru files in Git, a CLI-only Docker image, and no cloud sync by design

> Bruno is an MIT-licensed API client that stores collections as plain text .bru files in a folder you control, which is the whole difference from Postman. The same decision takes away shared workspaces, and the repository shows what fills the gap: sixteen workspaces, a separate CLI package, and Docker images published for the CLI alone.

**usebruno/bruno** — Opensource IDE For Exploring and Testing API's (lightweight alternative to Postman/Insomnia)

- Repository: https://github.com/usebruno/bruno
- Website: https://www.usebruno.com/
- Stars: 47,273 · Forks: 2,937
- Language: JavaScript
- License: MIT
- Published: 2026-08-08 · Updated: 2026-08-18 · Language: en
- Canonical page: https://hysenlabs.com/projects/usebruno-bruno

## Collections are .bru files in a folder, so Git is the only collaboration layer

The architecture is one decision carried through everything else. Bruno stores collections directly in a folder on your filesystem and writes them in a plain text markup language called Bru, so a request is a file with a name ending in .bru rather than a row in somebody's database. Version control is the collaboration story: the README says you can use Git, or any version control of your choice, over the collection.

What that buys is a reviewable diff. A changed header, a new body or an edited assertion shows up in a pull request as text, and a reviewer sees the request itself rather than a description of it.

What it costs is that nobody arbitrates a conflict for you. Two people editing the same .bru file produce a merge conflict in text with no server to resolve it and no visual diff tool behind a login, and the README points collaboration at your own repository rather than at a hosted workspace. That is a fine trade for a team already on Git and a poor one for a group of people who do not use it.

## Offline-only is a commitment, and it removes the shared workspace

The offline promise is stated as a position rather than a setting: Bruno is offline-only, there are no plans to add cloud-sync ever, and the reasoning given is that data should stay on your device. The long-term vision discussion is linked as the place where that thinking is written out.

Take it at face value and plan around it. There is no account, no shared collection store and no server holding environment variables, so every developer's machine must carry its own environment files, and a variable added for a new test target is a commit that each person has to pull and configure. Nothing in the project will tell you that a teammate's local environment is missing a value.

The upside is the mirror image of that problem: a collection cannot leak to a service you did not choose, because there is no service to leak to. Teams under a data residency or bring-your-own-storage rule find that argument useful. Teams that relied on a hosted Postman workspace for onboarding new engineers lose the shared starting point and have to document it themselves.

## Nine install routes, and the apt snippet ends at an unfinished redirect

Binaries are offered for Mac, Windows and Linux from the downloads page, and the same build is republished through package managers. The short ones:

```sh
# On Mac via Homebrew
brew install bruno

# On Windows via Chocolatey
choco install bruno

# On Windows via Scoop
scoop bucket add extras
scoop install bruno
```

```sh
# On Windows via winget
winget install Bruno.Bruno

# On Linux via Snap
snap install bruno

# On Linux via Flatpak
flatpak install com.usebruno.Bruno

# On Arch Linux via AUR
yay -S bruno
```

The Apt route is the one to look at twice, because the snippet in the README does not finish its own job:

```sh
# On Linux via 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 >
```

That last line ends in a redirect with no destination and the block stops there, so copying it gives you a keyring directory and a half-finished pipe. Finish that path from the documentation rather than guessing the missing argument. Note also that the package names differ per manager, com.usebruno.Bruno for Flatpak and Bruno.Bruno for winget, so a provisioning script has to hardcode one of them. Snap and Flatpak also put the application inside a sandbox, and the README does not say which filesystem access the app needs to read a collection folder, so a sandbox denial shows up as an empty collection rather than a clear error.

## bru run is the whole documented CLI, and the reference lives on the docs site

The command line tool is a separate npm package from the desktop application, which is what makes CI possible without shipping an IDE into a pipeline. It installs globally:

```sh
npm install -g @usebruno/cli
```

and then runs from the directory holding the collection, at three documented levels of scope:

```sh
# Run every request in the collection
bru run

# Run a single request
bru run request.bru

# Run a folder against a specific environment
bru run folder --env Local
```

The environment name is a positional value attached to a folder run, which means the environments themselves are files in the collection rather than a hosted list, and that the name in your CI script has to match a file on disk.

Everything past these three forms is documented elsewhere. The README defers the full command reference to the Bruno CLI documentation, and it does not cover exit codes, reporting formats, retries or how a failed assertion fails the build. If your pipeline needs to parse a result or gate on one, that behaviour has to come from the docs site, not from the repository.

## The published Docker images wrap the CLI, and the mount is the whole contract

Official images exist for the Bruno CLI and are published on every CLI release to two registries, with alpine and debian variants for linux/amd64 and linux/arm64. Their stated purpose is running collections in a pipeline or locally without installing Node.js or npm on the host.

```sh
# Pull from Docker Hub
docker pull usebruno/cli:latest

# Or pull from the GitHub Container Registry
docker pull ghcr.io/usebruno/cli:latest

# Run a collection by mounting the current directory
docker run -v $(pwd):/bruno usebruno/cli run
```

The mount line is the entire contract, and it constrains your repository layout. The collection has to sit under the directory you mount, and the command is written in POSIX shell syntax, so `$(pwd)` has to be replaced on a Windows runner. The image is the CLI, not the application: there is no graphical Bruno in a container, so a job that needs to record or eyeball a response has no path here. The tag matrix, environment files and worked CI examples for GitHub Actions, GitLab CI and Jenkins are all in the documentation, and pinning a digest rather than latest is your call, since the images move on every CLI release.

## Sixteen workspaces, an Electron shell, and two packages named like storage

The root package.json is private and declares sixteen workspaces under packages/. The names describe the split: bruno-electron is the desktop shell, bruno-app the interface, bruno-cli the command line tool, bruno-lang the Bru language, and bruno-schema and bruno-schema-types the format itself. Then there are bruno-requests, bruno-converters, bruno-toml, bruno-query, bruno-js, bruno-graphql-docs, bruno-common, bruno-tests, and two whose names point at persistence, bruno-sqlite and bruno-filestore.

An Electron shell explains the install weight and the update path: the desktop app arrives with a browser runtime inside it, which is where the size goes and why the package managers above matter more here than for a native binary.

The two storage packages are the part worth asking about. Collections are .bru files on disk, so something is also writing state into a local database, and the repository does not say what lives in it or whether it is portable across machines. A second data store next to a plain text format is exactly where a divergence between the file and the cache shows up, and it is worth asking before you commit a collection to a team.

## Bruno against Postman: the difference is where the collection lives

The project description calls Bruno a lightweight alternative to Postman and Insomnia, and the README frames it as an attempt to change the status quo those tools represent. The meaningful difference is not the request editor, it is the address of the data. Postman keeps collections attached to an account and a workspace; Bruno keeps them in a folder you version.

That has follow-on effects worth pricing before you move. Merge conflicts become your problem instead of the vendor's. Environment variables become committed files instead of a settings page. Request history is whatever your repository records. And a collection written in Postman's format is not a Bruno collection, so migrating is work rather than an import button: packages/bruno-converters exists in the workspace list, which tells you a conversion path was built, but the README documents no migration instructions and names no source formats it reads.

On features, the README is thin where it matters most. The Features section holds two headings with images and no text, and the commercial note says only that the majority of features are free and open source. What is verifiable from the repository is the file format, the CLI, the containers and the package manager list. Judge the rest against the pricing page and the documentation.

## MIT code, a trademarked name, and three minor releases since July

The code is MIT, stated in the repository's license.md, and the default branch is main with the last push on 2026-09-29. Releases moved from v4.0.0 on 2026-07-23 to v4.1.0 on 2026-08-20 and v4.2.0 on 2026-09-23, roughly monthly at minor versions, which for a tool that owns your API definitions means a scheduled upgrade habit rather than an occasional one.

Two things sit outside the MIT grant. The README states that Bruno is a trademark held by Anoop M D, so a fork that keeps the name is using someone else's mark, and the logo comes from OpenMoji under CC BY-SA 4.0, which is a share-alike licence on the asset rather than on the code. Neither stops you from contributing, but both need handling in a white-label build.

The repository also invests in its contributors. Twenty README translations live in docs/readme, and the root carries CODING_STANDARDS.md, governance.md, security.md, an eslint config, husky hooks, and three Playwright configurations including a benchmark one, so a patch runs through an end-to-end harness before it lands.

## Conclusion

Adopt Bruno if your team already keeps API definitions in Git and wants requests reviewable in a diff, because the .bru folder makes that the default rather than an export you regenerate. Do not adopt it if you need a shared cloud workspace or account-based history, since the project rules out cloud sync permanently and every environment file has to exist on each machine separately. Verify two things before you migrate a collection: which features sit behind the pricing page, since the README says only that the majority are free, and whether your existing collection survives the converters package, since packages/bruno-converters exists in the workspace list while no migration path is documented.

## FAQ

### How do I use Bruno for API testing?

Collections are folders on your filesystem written in a plain text markup language called Bru, and requests are run either from the desktop application or from the CLI with bru run, which executes every request in the collection. A single request or a folder can be run instead, and a folder run can be pointed at a named environment with bru run folder --env Local.

### How do I install Bruno?

Binaries for Mac, Windows and Linux are offered on the downloads page, and the same build is republished through package managers including Homebrew, Chocolatey, Scoop, Snap, Flatpak, Apt and the AUR. The Apt snippet in the README ends at an unfinished shell redirect, so take that route from the documentation instead.

### How do I install Bruno on a Mac?

The README gives a Homebrew command for the Mac, and also points to binary downloads on the project website for Mac, Windows and Linux. It does not give a curl or installer script for the desktop application.

### How do I install Bruno on Ubuntu?

Three Linux routes are given: Snap with snap install bruno, Flatpak with flatpak install com.usebruno.Bruno, and an Apt sequence that installs gpg and curl and imports a key into /etc/apt/keyrings, though the last line of that snippet is a redirect with no destination. The AUR line, yay -S bruno, covers Arch-based distributions.

### How do I use the Bruno CLI?

Install the separate npm package with npm install -g @usebruno/cli, then run bru from the directory holding your collection. The documented commands are bru run for the whole collection, bru run request.bru for one request, and bru run folder --env Local for a folder against a named environment.

### Can Bruno run collections in Docker for CI?

Yes, for the CLI. Official images are published on every CLI release to Docker Hub and the GitHub Container Registry, with alpine and debian variants for linux/amd64 and linux/arm64, and you mount the collection directory with docker run -v $(pwd):/bruno usebruno/cli run so no Node.js or npm is needed on the host.

## Sources

- [Official documentation](https://www.usebruno.com/)
- [Official README](https://github.com/usebruno/bruno#readme)
- [Project repository](https://github.com/usebruno/bruno)
- [Release notes](https://github.com/usebruno/bruno/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/usebruno-bruno
