Skip to content
Back to skills

Bruno

ASecurity

Tests and debugs APIs with Bruno, the open-source API client that keeps collections as plain files in a Git repository. Use when a user asks to create API requests, organize collections, write test scripts, use environments and variables, run a collection from the terminal or CI with the bru CLI, or collaborate on API workflows stored in Git.

  • 142 stars
  • 0 votes
  • 3 copies
  • 3 views
  • Added May 27, 2026
developmentrustgoshellbashnodeexpressdockertestinggitapi

Works with

  • terminal
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill bruno --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Bruno?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Bruno
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-bruno/badge)](https://www.skillsdirectory.com/skills/terminalskills-bruno)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: bruno
description: >-
  Tests and debugs APIs with Bruno, the open-source API client that keeps collections as plain files in a Git repository. Use when a user asks to create API requests, organize collections, write test scripts, use environments and variables, run a collection from the terminal or CI with the bru CLI, or collaborate on API workflows stored in Git.
license: Apache-2.0
compatibility: "Bruno desktop app 4.x (Windows, macOS, Linux). The bru CLI (@usebruno/cli 4.x) needs Node.js 20 or newer."
metadata:
  author: terminal-skills
  version: "1.1.0"
  category: development
  tags: ["api-client", "http", "testing", "git-friendly", "open-source"]
  repository: https://github.com/usebruno/bruno
---
# Bruno — Git-Friendly API Client

## Overview

Bruno is an open-source API client that stores collections as plain text files in a folder, so they live in the Git repository next to the code: versioned, reviewable in pull requests, with no account and no cloud sync (unlike Postman). The desktop app edits the files; the `bru` CLI runs them in a terminal or in CI.

A collection uses one of two file formats, never both:

- **OpenCollection YAML** (`.yml`, root file `opencollection.yml`) — the default for new and imported collections since Bruno 3.1.
- **Bru** (`.bru`, root file `bruno.json`) — the original format, still supported. The desktop app converts a Bru collection with **Migrate to YML** (Bruno 4.1 and later).

## Instructions

### Installation

```bash
npm install -g @usebruno/cli                   # CLI, Node.js 20+
bru --version                                  # 4.2.0
brew install bruno                             # desktop app, macOS
winget install Bruno.Bruno                     # desktop app, Windows
flatpak install flathub com.usebruno.Bruno     # desktop app, Linux
```

Other installers are on https://www.usebruno.com/downloads. The CLI is also published as the Docker image `usebruno/cli` (mount the collection at `/bruno`).

### Collection Structure

```
orders-api/
├── opencollection.yml        # Collection root (Bru format: bruno.json + collection.bru)
├── .env                      # Local secrets, listed in .gitignore
├── environments/             # local.yml, staging.yml
├── auth/
│   ├── folder.yml            # Folder settings (Bru format: folder.bru)
│   └── login.yml
├── users/                    # list-users.yml
└── orders/                   # create-order.yml
```

The root file `opencollection.yml` needs only `opencollection: 1.0.0` and an `info:` block with the collection `name`.

### Request File (YAML)

```yaml
# auth/login.yml
info:
  name: Login
  type: http
  seq: 1
  tags:
    - smoke

http:
  method: POST
  url: "{{baseUrl}}/api/auth/login"
  body:
    type: json
    data: |-
      {
        "email": "{{smokeEmail}}",
        "password": "{{smokePassword}}"
      }
  auth: inherit

runtime:
  scripts:
    - type: after-response
      code: |-
        if (res.status === 200) {
          bru.setVar("authToken", res.body.token);   // runtime variable for later requests
        }
    - type: tests
      code: |-
        test("login returns a token", () => {
          expect(res.status).to.equal(200);
          expect(res.body.token).to.be.a("string");
          expect(res.body.user.email).to.equal(bru.getEnvVar("smokeEmail"));
        });
```

Script types are `before-request`, `after-response` and `tests`. Tests use Chai's `expect`. `{{variable}}` is replaced in the URL, headers and body but not inside scripts: read values there with `bru.getEnvVar()` or `bru.getVar()`.

### Bru File Format

The same request in an older `.bru` collection. Bru has no comment syntax: a `#` line at the top of a file is a parse error and the CLI skips the file.

```bru
meta {
  name: Login
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/api/auth/login
  body: json
  auth: none
}

body:json {
  {
    "email": "{{smokeEmail}}",
    "password": "{{smokePassword}}"
  }
}

script:post-response {
  if (res.status === 200) {
    bru.setVar("authToken", res.body.token);
  }
}

tests {
  test("login returns a token", () => {
    expect(res.status).to.equal(200);
    expect(res.body.user.email).to.equal(bru.getEnvVar("smokeEmail"));
  });
}
```

### Environments

One file per environment in `environments/`. A value written as `{{process.env.NAME}}` comes from the shell or from a `.env` file at the collection root. A variable marked secret keeps its value outside the file (the desktop app stores it encrypted on the machine), so the CLI must receive it with `--env-var`.

```yaml
# environments/local.yml
name: local
variables:
  - name: baseUrl
    value: http://localhost:3000
  - name: smokeEmail
    value: maria.keller@northwind-logistics.com
  - name: smokePassword
    value: "{{process.env.SMOKE_USER_PASSWORD}}"
  - name: apiSecret
    secret: true
```

In a Bru collection the same file is `environments/local.bru`: a `vars { … }` block with one `baseUrl: http://localhost:3000` pair per line (a block written on a single line is a parse error) and a `vars:secret [ apiSecret ]` list for secret names.

### Scripting

```yaml
# orders/create-order.yml — sign the request before it is sent
info:
  name: Create order
  type: http
  seq: 1

http:
  method: POST
  url: "{{baseUrl}}/api/orders"
  headers:
    - name: X-Timestamp
      value: "{{timestamp}}"
    - name: X-Signature
      value: "{{signature}}"

runtime:
  scripts:
    - type: before-request
      code: |-
        const CryptoJS = require("crypto-js");
        const timestamp = Date.now().toString();
        const signature = CryptoJS.HmacSHA256(timestamp, bru.getEnvVar("apiSecret")).toString();
        bru.setVar("timestamp", timestamp);
        bru.setVar("signature", signature);
```

Scripts run in **Safe Mode** by default (CLI 3.0 and later). Safe Mode offers `require()` for the bundled libraries `chai`, `crypto-js`, `uuid`, `nanoid`, `moment`, `axios`, `jsonwebtoken`, `tv4`, `ajv`, `atob` and `btoa`. Node built-ins such as `crypto` and `fs`, npm packages, `lodash`, `xml2js` and `node-fetch` need Developer Mode: `bru run --sandbox=developer`.

Useful calls: `bru.setVar` / `bru.getVar` (runtime variables), `bru.getEnvVar` / `bru.setEnvVar`, `bru.getProcessEnv("NAME")`, `bru.runner.skipRequest()`, `bru.runner.stopExecution()`, `req.setHeader(name, value)`, `res.status`, `res.body`, `res.getHeader(name)`.

### CLI for CI/CD

```bash
cd orders-api                          # bru run works only at the collection root
bru run --env local                    # whole collection
bru run auth --env local               # one folder (add -r to include subfolders)
bru run auth/login.yml --env local     # one request
bru run --env local --tags=smoke       # only requests tagged "smoke"
# Pass a secret variable from the shell
bru run --env staging --env-var apiSecret="$ORDERS_API_SECRET"
# Stop at the first failure and write reports (the reports/ directory must already exist)
bru run --env staging --bail \
  --reporter-junit reports/results.xml --reporter-html reports/results.html
# Create a collection from an OpenAPI file (add --collection-format bru for .bru files)
bru import openapi --source openapi.yaml --output ./orders-api --collection-name "Orders API"
```

`bru run` exits with a non-zero code when a request gets no response or a test or assertion fails; a 4xx or 5xx response alone does not fail the run. The older `--output results.xml --format junit` pair still works but is deprecated in favour of the `--reporter-*` options.

## Examples

### Example 1: Add an authenticated request and run the collection

User: "Add a request to our Bruno collection that lists users with the token from login, then run everything locally."

```yaml
# users/list-users.yml
info:
  name: List users
  type: http
  seq: 1

http:
  method: GET
  url: "{{baseUrl}}/api/users"
  auth:
    type: bearer
    token: "{{authToken}}"

runtime:
  assertions:
    - expression: res.status
      operator: eq
      value: "200"
    - expression: res.body.data
      operator: isArray
```

```bash
cd orders-api                      # SMOKE_USER_PASSWORD is set in the shell or in .env
bru run auth users --env local
```

Result: the login request stores `authToken` and the next request sends it as a Bearer token. Running `bru run users --env local` alone returns 401, because nothing has set `authToken`.

```
auth/login (200 OK) - 12 ms
Tests
   ✓ login returns a token
users/list-users (200 OK) - 2 ms
Assertions
   ✓ res.status: eq 200
   ✓ res.body.data: isArray
```

### Example 2: Run the collection in GitHub Actions against staging

User: "Run our Bruno tests on every pull request and show the results in CI."

```yaml
# .github/workflows/api-tests.yml
on: pull_request
jobs:
  bruno:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: "24"
      - run: npm install -g @usebruno/cli
      - name: Run collection
        working-directory: orders-api
        env:
          SMOKE_USER_PASSWORD: ${{ secrets.SMOKE_USER_PASSWORD }}
          ORDERS_API_SECRET: ${{ secrets.ORDERS_API_SECRET }}
        run: |
          mkdir -p reports
          bru run --env staging \
            --env-var apiSecret="$ORDERS_API_SECRET" \
            --reporter-junit reports/results.xml
      - uses: actions/upload-artifact@v6
        if: ${{ !cancelled() }}
        with:
          name: bruno-report
          path: orders-api/reports/results.xml
```

Result: the job fails when any test or assertion fails, and `results.xml` holds one `<testsuite>` per request with a `<testcase>` for every test and assertion.

## Guidelines

1. **Git-first workflow** — Store Bruno collections in your repo next to application code; review API changes in PRs
2. **Environment files for config** — Use environments for base URLs; keep credentials out of them with `{{process.env.NAME}}` or secret variables, and add `.env` to `.gitignore`
3. **Secrets in the CLI** — Secret variables are stored by the desktop app, not in the collection, so a CLI run sees them empty unless `--env-var name=value` supplies them
4. **Test assertions** — Write tests in every request; run them in CI to catch API regressions
5. **Script chaining** — Use `bru.setVar()` in after-response scripts to pass data between requests (token → subsequent calls); the request that sets a variable has to run first
6. **Folder organization** — Mirror your API structure (auth/, users/, orders/); a folder can carry its own auth, variables and scripts in `folder.yml` (`folder.bru`)
7. **One format per collection** — Do not mix `.bru` and `.yml` requests; after migrating, change `.bru` paths passed to `bru.runRequest()` to `.yml`
8. **Safe Mode first** — Use `--sandbox=developer` only for collections you trust: Developer Mode scripts can read files and run system commands
9. **CI/CD integration** — Run `bru run --env staging` after deployment to verify the API contract; the exit code fails the pipeline. In CLI 4 the JUnit `classname` is the request's path in the collection, not its URL
10. **Reports can leak** — The JSON report records request and response headers and bodies, including a login password and the returned token; add `--reporter-skip-all-headers --reporter-skip-body` before publishing JSON or HTML reports as CI artifacts

Files in this skill

  • SKILL.md5.1 KB
  • _scores.json1.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…