Repository: usebruno/bruno

Issue:
Support environment file for CLI
### I have checked the following:

- [X] I've searched existing issues and found nothing related to my issue.

### Describe the feature you want to add

bruno-cli can support specifying an environment JSON file.

### Mockups or Images of the feature

e.g.`bru run --env-file {file_path}`

https://learning.postman.com/docs/collections/using-newman-cli/newman-options/#setup-options


Currently, we use the collection path to find the corresponding environment.

https://github.com/usebruno/bruno/blob/f871bc0fa28d0a1219ef3d078209eff18fdc854f/packages/bruno-cli/src/commands/run.js#L478

Please help to add a new `env-file` to allow a collection to use an environment JSON file.

https://github.com/usebruno/bruno/blob/f871bc0fa28d0a1219ef3d078209eff18fdc854f/packages/bruno-cli/src/commands/run.js#L391

[Jira](https://usebruno.atlassian.net/browse/BRU-1287)


Discussion:
Hi @moveurbody, thanks for the suggestion! Adding it to our development roadmap.
Dear Bruno developlement, I need the mentioned functionality also, as:

`bru run --env-file ../../global_environment.json`

**global_environment.json** is already created via the UI, but BrunoCLI (bru run) doesn't support it. 

The global environments are needed because Collections can have subfolders, and REST Calls (scripts) can use created/returned variables of other calls globally. The variable exchange between two subfolders of a collection is possible via UI and not via BrunoCLI. 

In short, please implement/complete the functionality considering these points. 

Happy to get feedback soon. regards!
Dear @sreelakshmi-bruno, is there an update on this issue? Or do you have a workaround that can help too?  
Hi @sreelakshmi-bruno,

Is there an update on this issue?
We are actively working on adding support for JSON environment files and this is expected to go in our next release.
Dear @anusree-bruno, thank you for the update. When do you expect the change will be available? If you have a pre-release version, I am glad to make some tests and give feedback to you. Best regards 
Hi @mesopotamiaMD , supporting JSON files in the CLI has been delayed since we are working on some other crucial tasks. Because of this reason, I cannot give you an ETA on this but we'll update here when we pick it up.


> The global environments are needed because Collections can have subfolders, and REST Calls (scripts) can use created/returned variables of other calls globally. The variable exchange between two subfolders of a collection is possible via UI and not via BrunoCLI.

I agree, Bruno CLI currently lacks global environment support, which creates a gap between UI and CLI functionality for variable exchange between subfolders.

Current Solutions:

You can achieve cross-subfolder variable exchange using Bruno's scripting functions:

**Runtime Variables:**
```javascript
// In Request A (any subfolder)
bru.setVar('authToken', response.body.token);

// In Request B (different subfolder)  
const token = bru.getVar('authToken');
```

**Environment Variables:**
```javascript
// In Request A
bru.setEnvVar('userID', response.body.userId);

// In Request B
const userID = bru.getEnvVar('userID');
```

These work great for **single-session workflows** where you need variables to flow between requests across different subfolders.


However, global environments solve a different problem for **persistence**: Variables survive between sessions (unlike `setVar`/`setEnvVar`) and**pre-configuration**: Available immediately without running setup scripts.

Will the approach suggested above solve your use case? Also would you be open to a call to understand your use case better and get some feedback? Thanks!

Hi @sreelakshmi-bruno, Thank you for your feedback. I will check if your advice can help me work around my issue and give feedback on it.  At this time, the ticket https://github.com/usebruno/bruno/issues/4251 was created, that what you mentioned didn't work. 


In any case, I will be available to show you my project at any time. 

If the workaround works there are still some points, that a global_environment.json

- persistence topic as you mentioned
- transfers from another application like postman, doesn't work easily. 
- for each collection call we needed repeated entries, that can be done only in one global_environment.json 

Best regards
Hi @mesopotamiaMD , thanks for getting back. Please pick a time that works for you from [here](https://cal.com/sreelakshmi-jayarajan/product-discovery). Thanks!



Patch:
diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js
index 261244949f..6d45315872 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -6,6 +6,7 @@ const { getRunnerSummary } = require('@usebruno/common/runner');
 const { exists, isFile, isDirectory } = require('../utils/filesystem');
 const { runSingleRequest } = require('../runner/run-single-request');
 const { getEnvVars } = require('../utils/bru');
+const { parseEnvironmentJson } = require('../utils/environment');
 const { isRequestTagsIncluded } = require("@usebruno/common")
 const makeJUnitOutput = require('../reporters/junit');
 const makeHtmlOutput = require('../reporters/html');
@@ -131,7 +132,7 @@ const builder = async (yargs) => {
       type: 'string'
     })
     .option('env-file', {
-      describe: 'Path to environment file (.bru) - can be absolute or relative path',
+      describe: 'Path to environment file (.bru or .json) - absolute or relative',
       type: 'string'
     })
     .option('env-var', {
@@ -306,7 +307,7 @@ const handler = async function (argv) {
           clientCertConfigJson = JSON.parse(clientCertConfigFileContent);
         } catch (err) {
           console.error(chalk.red(`Failed to parse Client Certificate Config JSON: ${err.message}`));
-          process.exit(constants.EXIT_STATUS.ERROR_INVALID_JSON);
+          process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
         }
 
         if (clientCertConfigJson?.enabled && Array.isArray(clientCertConfigJson?.certs)) {
@@ -346,10 +347,29 @@ const handler = async function (argv) {
         process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
       }
 
-      const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
-      const envJson = parseEnvironment(envBruContent);
-      envVars = getEnvVars(envJson);
-      envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env;
+      const ext = path.extname(envFilePath).toLowerCase();
+      if (ext === '.json') {
+        // Parse Bruno schema JSON environment
+        let envJsonContent;
+        try {
+          envJsonContent = fs.readFileSync(envFilePath, 'utf8');
+          const parsed = JSON.parse(envJsonContent);
+          const normalizedEnv = parseEnvironmentJson(parsed);
+          envVars = getEnvVars(normalizedEnv);
+          const rawName = normalizedEnv?.name;
+          const trimmedName = typeof rawName === 'string' ? rawName.trim() : '';
+          envVars.__name__ = trimmedName || path.basename(envFilePath, '.json');
+        } catch (err) {
+          console.error(chalk.red(`Failed to parse Environment JSON: ${err.message}`));
+          process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
+        }
+      } else {
+        // Default to .bru parsing
+        const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
+        const envJson = parseEnvironment(envBruContent);
+        envVars = getEnvVars(envJson);
+        envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env;
+      }
     }
 
     if (envVar) {
diff --git a/packages/bruno-cli/src/constants.js b/packages/bruno-cli/src/constants.js
index cdc0d47aa7..75c1be81c1 100644
--- a/packages/bruno-cli/src/constants.js
+++ b/packages/bruno-cli/src/constants.js
@@ -23,6 +23,8 @@ const EXIT_STATUS = {
   ERROR_INCORRECT_ENV_OVERRIDE: 8,
   // Invalid output format requested
   ERROR_INCORRECT_OUTPUT_FORMAT: 9,
+  // Invalid file format
+  ERROR_INVALID_FILE: 10,
   // Everything else
   ERROR_GENERIC: 255
 };
diff --git a/packages/bruno-cli/src/utils/environment.js b/packages/bruno-cli/src/utils/environment.js
new file mode 100644
index 0000000000..8989b89171
--- /dev/null
+++ b/packages/bruno-cli/src/utils/environment.js
@@ -0,0 +1,26 @@
+/**
+ * Parse a Bruno JSON environment object and normalize variables
+ * Accepts only single environment object: { name?, uid?, variables: [...] }
+ */
+const parseEnvironmentJson = (parsed = {}) => {
+  if (!parsed || !Array.isArray(parsed.variables)) {
+    throw new Error('Invalid environment JSON: expected a single environment object with a "variables" array');
+  }
+
+  const normalized = {
+    name: parsed.name,
+    variables: (parsed.variables || []).filter(Boolean).map((variable) => ({
+      name: variable.name,
+      value: variable.value,
+      type: variable.type || 'text',
+      enabled: variable.enabled !== false,
+      secret: variable.secret || false
+    }))
+  };
+
+  return normalized;
+};
+
+module.exports = {
+  parseEnvironmentJson
+};


Test patch:
diff --git a/packages/bruno-cli/tests/utils/parse-environment-json.spec.js b/packages/bruno-cli/tests/utils/parse-environment-json.spec.js
new file mode 100644
index 0000000000..9eb466a97d
--- /dev/null
+++ b/packages/bruno-cli/tests/utils/parse-environment-json.spec.js
@@ -0,0 +1,91 @@
+const { describe, it, expect } = require('@jest/globals');
+const { getEnvVars } = require('../../src/utils/bru');
+const { parseEnvironmentJson } = require('../../src/utils/environment');
+
+describe('parseEnvironmentJson', () => {
+  it('normalizes single environment object', () => {
+    const input = {
+      name: 'My Env',
+      variables: [
+        { name: 'host', value: 'https://www.httpfaker.org' },
+        { name: 'token', value: 'abc', enabled: false, secret: true }
+      ]
+    };
+    const env = parseEnvironmentJson(input);
+    expect(Array.isArray(env.variables)).toBe(true);
+    expect(env.variables[0]).toEqual({
+      name: 'host',
+      value: 'https://www.httpfaker.org',
+      type: 'text',
+      enabled: true,
+      secret: false
+    });
+    expect(env.variables[1].enabled).toBe(false);
+    expect(env.variables[1].secret).toBe(true);
+
+    const vars = getEnvVars(env);
+    expect(vars).toEqual({ host: 'https://www.httpfaker.org' });
+  });
+
+  it('throws on invalid shape', () => {
+    expect(() => parseEnvironmentJson({ name: 'x' })).toThrow(/Invalid environment JSON/i);
+  });
+
+  it('respects explicit fields and preserves secret flag', () => {
+    const input = {
+      name: 'My Env',
+      variables: [
+        { name: 'one', value: '1', type: 'text', enabled: true, secret: true },
+        { name: 'two', value: '2', type: 'file', enabled: false, secret: false }
+      ]
+    };
+    const env = parseEnvironmentJson(input);
+
+    expect(env.variables[0]).toEqual({
+      name: 'one',
+      value: '1',
+      type: 'text',
+      enabled: true,
+      secret: true
+    });
+    expect(env.variables[1]).toEqual({
+      name: 'two',
+      value: '2',
+      type: 'file',
+      enabled: false,
+      secret: false
+    });
+
+    const vars = getEnvVars(env);
+    expect(vars).toEqual({ one: '1' });
+  });
+
+  it('defaults secret to false for undefined and null', () => {
+    const input = {
+      name: 'My Env',
+      variables: [
+        { name: 'three', value: '3', enabled: true },
+        { name: 'four', value: '4', enabled: true, secret: null }
+      ]
+    };
+    const env = parseEnvironmentJson(input);
+
+    expect(env.variables[0]).toEqual({
+      name: 'three',
+      value: '3',
+      type: 'text',
+      enabled: true,
+      secret: false
+    });
+    expect(env.variables[1]).toEqual({
+      name: 'four',
+      value: '4',
+      type: 'text',
+      enabled: true,
+      secret: false
+    });
+
+    const vars = getEnvVars(env);
+    expect(vars).toEqual({ three: '3', four: '4' });
+  });
+});
diff --git a/tests/runner/cli-json-env-file/cli-json-env-file.spec.ts b/tests/runner/cli-json-env-file/cli-json-env-file.spec.ts
new file mode 100644
index 0000000000..080dedc8bf
--- /dev/null
+++ b/tests/runner/cli-json-env-file/cli-json-env-file.spec.ts
@@ -0,0 +1,61 @@
+import { test, expect } from '../../../playwright';
+import { execSync } from 'child_process';
+import fs from 'fs';
+import path from 'path';
+import constants from '../../../packages/bruno-cli/src/constants.js';
+
+test.describe('CLI JSON Environment File Support', () => {
+  const collectionPath = path.resolve(__dirname, 'collection');
+  const BRU = 'node ../../../../packages/bruno-cli/bin/bru.js';
+
+  // Helper: emulate `bru run` from a given working directory and
+  // return the process exit code (0 on success). We use execSync so
+  // these tests behave like invoking the CLI directly in a shell.
+  const runFrom = (cwd: string, args: string): number => {
+    try {
+      execSync(`cd "${cwd}" && ${BRU} ${args}`, { stdio: 'pipe' });
+      return 0;
+    } catch (error: any) {
+      return error?.status ?? 1;
+    }
+  };
+
+  test('CLI: Run with invalid JSON environment file should fail', async () => {
+    // Create a temporary invalid JSON file
+    const tempDir = '/tmp/bruno-cli-test';
+    const invalidEnvPath = path.join(tempDir, 'invalid-env.json');
+
+    fs.mkdirSync(tempDir, { recursive: true });
+    fs.writeFileSync(invalidEnvPath,
+      JSON.stringify({
+        name: 'Invalid Env'
+        // missing variables array - invalid JSON
+      }));
+
+    const status = runFrom(collectionPath, `run --env-file "${invalidEnvPath}"`);
+    expect(status).toBe(constants.EXIT_STATUS.ERROR_INVALID_FILE);
+    try {
+      // Cleanup
+      fs.unlinkSync(invalidEnvPath);
+      fs.rmdirSync(tempDir);
+    } catch (e) {}
+  });
+
+  test('CLI: Run with valid JSON env and interpolates variables', async () => {
+    const envPath = path.join(collectionPath, 'env.json');
+    const outputPath = path.join(collectionPath, 'out.json');
+
+    // Even if exit is non-zero (network warnings), reporter should be written
+    runFrom(collectionPath, `run request.bru --env-file "${envPath}" --reporter-json "${outputPath}"`);
+
+    expect(fs.existsSync(outputPath)).toBe(true);
+    const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
+    const result = report.results[0];
+    expect(result.request.url).toBe('https://echo.usebruno.com');
+    expect(result.response.status).toBe(200);
+
+    try {
+      fs.unlinkSync(outputPath);
+    } catch (_) {}
+  });
+});
diff --git a/tests/runner/cli-json-env-file/collection/bruno.json b/tests/runner/cli-json-env-file/collection/bruno.json
new file mode 100644
index 0000000000..368998d232
--- /dev/null
+++ b/tests/runner/cli-json-env-file/collection/bruno.json
@@ -0,0 +1,6 @@
+{
+  "version": "1",
+  "name": "cli-json-env-fixture",
+  "type": "collection"
+}
+
diff --git a/tests/runner/cli-json-env-file/collection/env.json b/tests/runner/cli-json-env-file/collection/env.json
new file mode 100644
index 0000000000..a61a140079
--- /dev/null
+++ b/tests/runner/cli-json-env-file/collection/env.json
@@ -0,0 +1,4 @@
+{
+  "name": "CLI JSON Env",
+  "variables": [{ "name": "baseUrl", "value": "https://echo.usebruno.com", "enabled": true }]
+}
diff --git a/tests/runner/cli-json-env-file/collection/request.bru b/tests/runner/cli-json-env-file/collection/request.bru
new file mode 100644
index 0000000000..278e47876b
--- /dev/null
+++ b/tests/runner/cli-json-env-file/collection/request.bru
@@ -0,0 +1,17 @@
+meta {
+  name: cli-json-env-test
+  type: http
+}
+
+http {
+  method: POST
+  url: {{baseUrl}}
+  body: json
+  auth: none
+}
+
+body:json {
+  {
+    "ping": "pong"
+  }
+}
\ No newline at end of file