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
  • 6 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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.