Bruno V4 changes how secrets are stored and how environment variables behave. It also adds new optional fields to requests and environments like descriptions and variable types so their schema changes slightly.
Some of these changes require action before upgrading; others are just good to know if your team works across mixed Bruno versions.
Have feedback? Join the discussion on GitHub.
⚠️ Breaking Changes: V4 includes breaking changes to secret storage, environment variable persistence, WebSocket message format, and CLI JUnit output. Please review the sections below before upgrading.
Action required for CLI / CI users
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, and is managed from the Environment UI instead of from Collection Settings > Secrets.
You also get a simpler way to reference a secret - by its name directly:
| Syntax | Status |
|---|---|
| {{name.keyname}} | New (recommended) |
| {{$secrets.name.keyname}} | Old (still works, deprecated) |
Both resolve to the same value for now, so nothing breaks immediately. The old {{$secrets.*}} form is deprecated and will be removed after 3 months. Wherever the old syntax appears, 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. Keeping configuration alongside the environment it belongs to is clearer, removes a separate root-level file, and makes the environment the single source of truth. It also brings full secret manager support to the CLI across AWS, Azure, and Vault, and adds support for workspace and global environments, which the old secrets.json model could not express.
Bruno app (GUI) - nothing required
Migration is automatic. When you open a collection in V4, Bruno moves your secret configuration from secrets.json into the relevant environment files in the background, then deletes secrets.json once migration succeeds. Your secret values are never written to disk - only the mappings and provider configuration move.
CLI / CI - action required
The CLI does not migrate on its own. If your collection still uses a secrets.json, your run will show a warning to open the collection in the Bruno app and go to Collection Settings > Secrets to complete migration. The warning does not block the run. Once you've migrated in the app, pass the environment to your CLI run: bru run --env <name>
Before - secrets.json at collection root
{
"type": "aws-secrets-manager",
"data": [
{
"environment": "Production",
"secrets": [
{ "name": "dbPassword", "secretName": "prod/db/credentials", "enabled": true },
{ "name": "apiKey", "secretName": "prod/payment-gateway/api-key", "enabled": true }
]
}
]
}After - YAML: 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-gateway/api-key
enabled: trueAfter - BRU: environments/staging.bru
vars {
baseUrl: https://staging.api.example.com
}
vars:externalsecrets:aws-secrets-manager {
dbPassword: staging/db/credentials
~apiKey: staging/payment-gateway/api-key
}Action required if scripts set sensitive values
In V4, scripts that modify environment variables automatically persist those changes to disk. This makes script-driven variable updates consistent and predictable.
Watch out: 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.
You are affected if:
bru.setEnvVar(), bru.deleteEnvVar(), or bru.setGlobalEnvVar() - these did not write to disk before V4, and now they doYou are not affected if:
bru.setEnvVar(), bru.deleteEnvVar(), and bru.setGlobalEnvVar() usage that handles sensitive valuesbru.setVar() / bru.deleteVar(), which keep the value in memory for the run instead of persisting itNo migration needed - watch out on mixed-version teams
In V4, you can add multiple WebSocket messages to a single request. To support this, Bruno saves requests in a new message format. Existing requests keep working, and you can now define more than one message per WebSocket request.
Mixed-version teams: Collections using the new format may not load correctly in Bruno V3.5.0 or earlier. This affects only YAML collections with WebSocket requests - BRU format is not impacted.
V3.5.0 - BRU (single message)
body:ws {
type: <json | text>
content: '''
<message payload>
'''
}V4 - BRU (multiple messages)
body:ws {
name: <message title>
type: <json | text>
selected: <true | false>
content: '''
<message payload>
'''
}V3.5.0 - YAML
message:
type: <json | text>
data: "<message payload>"V4 - YAML
message:
- title: <message title>
selected: <true | false>
message:
type: <json | text>
data: |-
<message payload>No migration needed - watch out on mixed-version teams
In V4, you can add descriptions to params, headers, multipart form fields, and variables, and assign types (@string, @number, @boolean, @object) to your variables. Bruno stores these as annotations on the line above each pair.
How older versions handle it:
Pre-V4 - BRU
headers {
Content-Type: application/json
Authorization: Bearer {{token}}
}
params:query {
page: 1
}
vars:pre-request {
baseUrl: https://api.example.com
retryCount: 3
}V4 - BRU
headers {
@description('Payload format for the request')
Content-Type: application/json
@description('Bearer token used for auth')
Authorization: Bearer {{token}}
}
params:query {
@description('Page number to fetch')
page: 1
}
vars:pre-request {
@string
@description('Base URL for all requests')
baseUrl: https://api.example.com
@number
retryCount: 3
}Pre-V4 - YAML
http:
headers:
- name: Content-Type
value: application/json
- name: Authorization
value: Bearer {{token}}
params:
- name: page
value: "1"
type: query
runtime:
variables:
- name: baseUrl
value: https://api.example.com
- name: retryCount
value: "3"V4 - YAML
http:
headers:
- name: Content-Type
value: application/json
description: Payload format for the request
- name: Authorization
value: Bearer {{token}}
description: Bearer token used for auth
params:
- name: page
value: "1"
type: query
description: Page number to fetch
runtime:
variables:
- name: baseUrl
value: https://api.example.com
description: Base URL for all requests
- name: retryCount
value:
type: number
data: "3"Action required if you consume CLI JUnit output in CI
In the CLI JUnit reporter, each test case's classname attribute changes from the request URL to the collection path to the request - for example, Users/Get Users for a request named "Get Users" inside a "Users" folder.
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 in staging vs. production and showed up as duplicate test cases. The collection path is stable across environments.
classname expecting a URLBefore - URL (varies per environment)
<testcase
name="Status is 200"
classname="https://prod.api.example.com/users"
/>After - collection path (stable)
<testcase
name="Status is 200"
classname="Users/Get Users"
/>bru.setEnvVar(), bru.deleteEnvVar(), and bru.setGlobalEnvVar() usage for sensitive values, and move those to bru.setVar() / bru.deleteVar()classname from the CLI JUnit report and expects a URL, update it to expect the collection path (Folder/Request Name) insteadsecrets.json, then go to Collection Settings > Secrets > Go to Environment to verify{{$secrets.*}} references to {{name.keyname}} when convenient (within the 3-month window)Download the latest version and enjoy all the new features and improvements.
Download Bruno V4Have questions or feedback? Join the discussion on GitHub