Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .bumpy/json-full-exclude-internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
varlock: patch
---

Fix: `varlock load --format json-full` no longer includes `@internal` items by default (pass `--include-internal` to opt in for local debugging). Framework integrations shell out to this exact command to get their injected config, so this closes a leak where an `@internal` secret-zero credential could reach client/SSR runtime code.
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ varlock load [options]
- `--agent`: Agent-safe mode: defaults to JSON output and redacts sensitive values. Not compatible with `--format env` or `--format shell`.
- `--compact`: Compact output (json-full: no indentation, env/shell: skip undefined values)
- `--show-all`: Shows all items, not just failing ones, when validation is failing
- `--include-internal`: Include [`@internal`](/reference/item-decorators/#internal) items in `--format json-full` output. Excluded by default (`json-full` is commonly consumed programmatically, e.g. by framework integrations, not just for local human inspection) - pass this for local debugging of a secret-zero credential.
- [Common options](#common-options): `--env`, `--path` / `-p`, `--clear-cache`, `--skip-cache`

**Examples:**
Expand Down Expand Up @@ -146,7 +147,7 @@ varlock load --agent
**Output formats (for scripts & agents):**
- `--format json`: a flat `{ "KEY": value }` map of resolved values on stdout
- `--agent`: the same flat map, but `@sensitive` values are redacted (e.g. `"su▒▒▒▒▒"`), so it is safe to print in logs or agent transcripts. Implies JSON output; not compatible with `--format env`/`shell`. Combine with `--agent --format json-full` to get the redacted full graph.
- `--format json-full`: the full serialized graph: top-level `basePath`, `sources`, `config` (per-item metadata including resolved value, validation state, and sensitivity), `settings`. Use this when you need per-item validation/error detail rather than just values. ⚠️ This includes **raw resolved secret values**, so add `--agent` (`--agent --format json-full`) to redact them before logging or feeding to an agent.
- `--format json-full`: the full serialized graph: top-level `basePath`, `sources`, `config` (per-item metadata including resolved value, validation state, and sensitivity), `settings`. Use this when you need per-item validation/error detail rather than just values. ⚠️ This includes **raw resolved secret values**, so add `--agent` (`--agent --format json-full`) to redact them before logging or feeding to an agent. [`@internal`](/reference/item-decorators/#internal) items are excluded unless you pass `--include-internal`.
- `--format env` / `--format shell`: dotenv lines / shell `export` statements with **raw** values. Never pipe these somewhere that gets logged when secrets are involved.

When emitting machine-readable output, add `--summary-stderr` (or `--summary-file`) to get a human-readable, redacted summary on **stderr** while keeping clean JSON on **stdout**. This is handy for agents and CI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ Because internal items still resolve and validate, marking a variable internal d
:::

`varlock run` strips internal items from the child process environment **even if they were set in the ambient environment** (e.g. `OP_TOKEN=xxx varlock run ...`), so a secret-zero token never leaks into your app. If a _nested_ `varlock run` needs the internal value for its own resolution, pass [`--include-internal`](/reference/cli-commands/#run) to keep it in the child env.

`varlock load --format json-full` excludes internal items by default too, since that format is commonly consumed programmatically (framework integrations shell out to this exact command to get their injected config) - pass [`--include-internal`](/reference/cli-commands/#load) there for local debugging.
</div>

<div>
Expand Down
14 changes: 11 additions & 3 deletions packages/varlock/src/cli/commands/load.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export const commandSpec = define({
type: 'boolean',
description: 'When load is failing, show all items rather than only failing items',
},
'include-internal': {
type: 'boolean',
description: 'Include @internal items in --format json-full output (excluded by default, since json-full is commonly consumed programmatically - e.g. by framework integrations - not just for local human inspection)',
},
env: {
type: 'string',
description: 'Set the environment (e.g., production, development, etc) - will be overridden by @currentEnv in the schema if present',
Expand Down Expand Up @@ -77,6 +81,7 @@ Examples:
varlock load --format json-full --summary-stderr # JSON on stdout + redacted human summary on stderr
varlock load --format json-full --summary-file /tmp/summary.txt # JSON on stdout + redacted human summary written to file
varlock load --agent # Agent-safe JSON output with sensitive values redacted
varlock load --format json-full --include-internal # Include @internal items for local debugging
`.trim(),
});

Expand All @@ -93,6 +98,7 @@ export function formatShellValue(value: string): string {
export const commandFn: TypedGunshiCommandFn<typeof commandSpec> = async (ctx) => {
const {
format, compact, 'show-all': showAll, 'summary-stderr': summaryStderr, 'summary-file': summaryFile, agent,
'include-internal': includeInternal,
} = ctx.values;
// --agent defaults to json if no explicit --format was set, but respects --format if provided
const outputFormat = agent && format === 'pretty' ? 'json' : format;
Expand Down Expand Up @@ -189,9 +195,11 @@ export const commandFn: TypedGunshiCommandFn<typeof commandSpec> = async (ctx) =
console.log(JSON.stringify(env, null, 2));
} else if (outputFormat === 'json-full') {
const indent = compact ? 0 : 2;
// this is an inspection dump (not the injected blob), so include @internal items —
// they're flagged with isInternal and redacted below like any other sensitive value
const serialized = envGraph.getSerializedGraph({ includeInternal: true });
// @internal items are excluded by default, same as every other format — json-full is
// routinely consumed programmatically (framework integrations shell out to this exact
// command to get their injected config), so a secret-zero credential must not appear here
// unless explicitly requested. Pass --include-internal for local human inspection.
const serialized = envGraph.getSerializedGraph({ includeInternal: !!includeInternal });
if (agent) {
for (const key in serialized.config) {
const item = serialized.config[key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('@internal item decorator', () => {
OP_TOKEN=abc123
DB_URL=concat("x-", ref(OP_TOKEN))
`);
// inspection output (e.g. `load --format json-full`) opts in and tags the item
// inspection output (e.g. `load --format json-full --include-internal`) opts in and tags the item
const inspect = g.getSerializedGraph({ includeInternal: true });
expect(inspect.config.OP_TOKEN).toMatchObject({ value: 'abc123', isInternal: true });
// non-internal items are not tagged
Expand Down
23 changes: 23 additions & 0 deletions smoke-tests/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,29 @@ describe('CLI Commands', () => {
expect(result.exitCode).toBe(0);
});

test('--format json-full excludes @internal vars by default', () => {
// framework integrations shell out to exactly this command to get their injected config -
// an @internal secret-zero credential must not appear here unless explicitly requested
const result = runVarlock(['load', '--format', 'json-full'], {
cwd: 'smoke-test-internal',
env: { OP_TOKEN: 'secret-zero' },
});
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout);
expect(parsed.config).not.toHaveProperty('OP_TOKEN');
expect(parsed.config.PUBLIC_VAR.value).toBe('visible');
});

test('--format json-full --include-internal includes @internal vars, flagged', () => {
const result = runVarlock(['load', '--format', 'json-full', '--include-internal'], {
cwd: 'smoke-test-internal',
env: { OP_TOKEN: 'secret-zero' },
});
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout);
expect(parsed.config.OP_TOKEN).toMatchObject({ value: 'secret-zero', isInternal: true });
});

// `--no-inject-graph` is deprecated and hidden from help, but still supported for
// back-compat. Guard that it keeps working and omits the __VARLOCK_ENV blob.
test('--no-inject-graph still works and omits the __VARLOCK_ENV blob', () => {
Expand Down
Loading