Bruno v4 brings meaningful improvements to how secrets are stored, how environment variables behave, and what the collection schema supports. Most of the changes are automatic when you upgrade through the app. But a handful of them require attention before you upgrade, especially if your scripts touch sensitive values or your team runs mixed Bruno versions. This guide walks through every change, who is affected, and exactly what to do.
Table of Contents
| Change | Action needed? | Who is affected |
|---|---|---|
| Secret Manager config moves into environment files | App users: automatic. CLI users: open app first. | Anyone using external secrets |
| Script-set env vars persist to disk by default | Yes — audit before upgrading | Scripts calling setEnvVar / deleteEnvVar |
| Multiple WebSocket messages per request | Nothing to migrate; plan for mixed-version teams | Teams with YAML WebSocket collections |
| Descriptions and variable types in schema | Nothing to migrate; plan for mixed-version teams | Teams using older Bruno versions |
| CLI JUnit classname is now the request path | Yes — update CI tooling that reads classname | CI pipelines using JUnit XML output |
In v4, external secrets configuration no longer lives in a secrets.json file at the root of your collection. It moves into your environment files, under a new externalSecrets section, managed from the Environment UI instead of from Collection Settings > Secrets.
Only the secret manager configuration moves. Your actual secret values are never written into the collection. Secret encryption and storage work the same way as before.
You also get a simpler way to reference a secret by its name directly:
| Syntax | Status |
|---|---|
{{name.keyname}} |
New (recommended) |
{{$secrets.name.keyname}} |
Old (deprecated, works for 3 months) |
Both forms resolve to the same value for now, so nothing breaks immediately. The old {{$secrets.*}} form is deprecated and support is removed after three months. Wherever the old syntax appears in your scripts and requests, the app marks it with an underline and a hover tooltip pointing you to the new form.
Secrets are environment-specific. Your Production token is not your Staging token, so keeping the configuration alongside the environment it belongs to is clearer. It removes a separate root-level file to manage, makes the environment the single source of truth for secrets, and brings full secret-manager support to the CLI across AWS, Azure, and Vault. The old secrets.json model also could not support workspace and global environments — the new model can.
Using the Bruno app (GUI)
Nothing to do. Migration is automatic. When you open a collection in v4, Bruno moves your secret config from secrets.json into the relevant environment files and deletes secrets.json once migration succeeds. Only the mappings and provider config move, never the secret values.
Using the CLI or CI
The CLI does not migrate on its own. If your collection still uses a secrets.json, your run shows a warning. Open the collection in the Bruno app first to complete the migration, then your CLI and CI runs pick up the config from the environment files — pass the environment name via bru run --env <name>. The warning does not block the run.
Before — secrets.json at the collection root:
{
"type": "aws-secrets-manager",
"data": [
{
"environment": "Production",
"secrets": [
{ "name": "dbPassword", "secretName": "prod/db/credentials", "enabled": true },
{ "name": "apiKey", "secretName": "prod/payment/api-key", "enabled": true }
]
}
]
}
After — YAML format, environments/production.yml:
name: Production
variables:
- name: baseUrl
value: https://api.example.com
externalSecrets:
type: aws-secrets-manager
variables:
- name: dbPassword
secretName: prod/db/credentials
enabled: true
- name: apiKey
secretName: prod/payment/api-key
enabled: true
After — BRU (legacy) format, environments/staging.bru:
vars {
baseUrl: https://staging.api.example.com
}
vars:externalsecrets:aws-secrets-manager {
dbPassword: staging/db/credentials
~apiKey: staging/payment/api-key
}
In v4, scripts that modify environment variables automatically persist those changes to disk. This makes script-driven variable updates consistent and predictable. The thing to watch out for is that if your scripts set tokens, credentials, or other sensitive values with bru.setEnvVar(), bru.deleteEnvVar(), or bru.setGlobalEnvVar(), those values will now be written to your environment file and could be committed to version control by accident.
| API | Persisted to disk before v4? | Persisted in v4? |
|---|---|---|
bru.setEnvVar() |
No (memory only, unless { persist: true } passed) |
Yes |
bru.deleteEnvVar() |
No | Yes |
bru.setGlobalEnvVar() |
Yes, but strings only — non-strings dropped on save | Yes, all types |
bru.setVar() |
Never (memory only) | Still memory only |
bru.deleteVar() |
Never (memory only) | Still memory only |
You are affected if any of these apply
bru.setEnvVar() or bru.deleteEnvVar()setEnvVar() values stayed in memory onlyYou are not affected if
Audit your scripts
Search your collection for calls to bru.setEnvVar(), bru.deleteEnvVar(), and bru.setGlobalEnvVar(). Note any that write sensitive values.
Switch sensitive calls to in-memory APIs
For sensitive values, replace setEnvVar() with bru.setVar() and deleteEnvVar() with bru.deleteVar(). These keep the value in memory for the duration of the run only.
Use the v4 migration helper in v3.5.0
In Bruno v3.5.0, enable the v4 migration option in the footer panel. It highlights these API calls in the sidebar, scripts, and tests so you can find and update the sensitive ones before upgrading.
Non-sensitive values do not need to change. If your script sets a baseUrl, a page counter, or a request ID, persisting it to disk is fine and you do not need to do anything.
In v4 you can add multiple WebSocket messages to a single request. To support this, Bruno saves YAML WebSocket requests in a new message format. BRU (legacy) format collections are not affected by this schema change.
Nothing to migrate before upgrading. Your existing WebSocket requests keep working. The main thing to plan for is mixed-version teams.
v3.5.0 (single message)
message:
type: json
data: "{ \"action\": \"ping\" }"
v4 (multiple messages)
message:
- title: Ping message
selected: true
message:
type: json
data: |-
{ "action": "ping" }
If some teammates are on older versions and you save a WebSocket request with multiple messages in v4, they may not load that request correctly in v3.5.0 or earlier. The simplest fix is to coordinate the upgrade across the team.
In v4 you can add descriptions to params, headers, multipart form fields, and variables, and assign types to variables. Bruno stores these as annotations on the line above each pair: descriptions as @description('...'), and types as @string, @number, @boolean, or @object.
Nothing to migrate before upgrading. The mixed-version caveat below is the main thing to plan for.
| Bruno version | What happens when opening a v4 collection |
|---|---|
| v3.4.0 to pre-v4 | Collection opens fine. Descriptions are not shown in the UI and variables do not get type handling, but nothing breaks. |
| Below v3.4.0 | The older parser does not understand the annotation syntax. Affected requests may fail to load, and environments may appear to be missing. |
Before v4 — BRU format
headers {
Content-Type: application/json
Authorization: Bearer
}
vars:pre-request {
baseUrl: https://api.example.com
retryCount: 3
}
v4 — BRU format with annotations
headers {
@description('Payload format')
Content-Type: application/json
@description('Auth token')
Authorization: Bearer
}
vars:pre-request {
@string
@description('Base URL for all requests')
baseUrl: https://api.example.com
@number
retryCount: 3
}
Before v4 — YAML format
runtime:
variables:
- name: baseUrl
value: https://api.example.com
- name: retryCount
value: "3"
v4 — YAML format with types
runtime:
variables:
- name: baseUrl
value: https://api.example.com
description: Base URL for all requests
- name: retryCount
value:
type: number
data: "3"
In the CLI JUnit reporter, each test case's classname attribute changes from the request URL to the collection path including the request name. For example, a request called Get Users inside a Users folder would produce Users/Get Users as the classname.
The reason for this change is that CI tools like Xray and Azure DevOps use classname + name as a test's unique identifier. Because the URL changes per environment, the same test produced a different classname when run against staging versus production, and it showed up as duplicate test cases. The collection path is stable across environments so the same test now maps to a single entry.
Before (URL, varies per environment)
<!-- Staging run -->
<testcase
name="Status is 200"
classname="https://staging.api.example.com/users"
/>
<!-- Production run -->
<testcase
name="Status is 200"
classname="https://prod.api.example.com/users"
/>
After (stable collection path)
<!-- Staging run -->
<testcase
name="Status is 200"
classname="Users/Get Users"
/>
<!-- Production run -->
<testcase
name="Status is 200"
classname="Users/Get Users"
/>
What you need to do: If your CI tooling or scripts read classname expecting a URL, update them to expect the collection path format instead. If you only need the request name, extract it from the last segment of the path. You are not affected if you do not consume the CLI's JUnit output or do not key on classname.
Work through this list before upgrading any machine in your team.
Audit scripts for sensitive env var calls
Find every bru.setEnvVar(), bru.deleteEnvVar(), and bru.setGlobalEnvVar() call that writes a sensitive value, and move those to bru.setVar() / bru.deleteVar(). Use the v4 migration helper in v3.5.0 to highlight them automatically.
Coordinate the upgrade across mixed-version teams
If someone saves a collection in v4 and a teammate opens it in an older version, the following can break:
secrets.json.Update CI tooling that reads classname from JUnit XML
If your pipeline reads classname from the CLI JUnit report and expects a URL, update it to expect the collection path format Folder/Request Name instead.
Verify secret config migration in the app
If your collection uses an external secret manager, open it in the Bruno app so the secret config migrates out of secrets.json. Then go to Collection Settings > Secrets > Go to Environment to confirm the secrets migrated successfully.
Do the migration before running from the CLI
If you run collections from the CLI, complete the in-app migration first. Once done, your CLI runs pick up the secret config from the environment files when you pass bru run --env <name>.
Update deprecated secret references when convenient
Replace {{$secrets.name.keyname}} with {{name.keyname}} across your requests and scripts. The old form works for three months so there is no urgency, but the app marks each occurrence with an underline and tooltip to make them easy to find.
Audit your scripts, coordinate your team, and the upgrade to v4 is straightforward.