You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: ${VAR} env-var interpolation for configs (closes#635) (#655)
* fix: [#635] accept ${VAR} shell/dotenv syntax for env interpolation
Config substitution previously only accepted {env:VAR}. Users arriving
from Claude Code, VS Code, dotenv, or docker-compose naturally write
${VAR} and hit silent failures — the literal string passes through to
MCP servers as the env value, shadowing the forwarded parent env.
This adds ${VAR} as an alias for {env:VAR}. Regex matches POSIX
identifier names only ([A-Za-z_][A-Za-z0-9_]*) to avoid collisions
with random ${...} content in URLs or paths. Bare $VAR (without
braces) is intentionally NOT interpolated — too collision-prone.
- paths.ts: add second regex replace after the existing {env:VAR} pass
- paths-parsetext.test.ts: 6 new tests covering shell syntax, mixed
use, invalid identifier names, bare $VAR rejection, and MCP env
regression scenario
- mcp-servers.md: document both syntaxes with a table
Closes#635
* fix: address consensus review findings for PR #655
Applies fixes from multi-model consensus review (Claude + GPT 5.4 + Gemini 3.1 Pro).
- C1 (CRITICAL — JSON injection): ${VAR} substitution is now JSON-safe via
JSON.stringify(value).slice(1, -1). Env values with quotes/commas/newlines
can no longer break out of the enclosing JSON string. Existing {env:VAR}
keeps raw-injection semantics for backward compat (documented as the
opt-in power-user syntax).
- M2 (MAJOR — escape hatch): $${VAR} now preserves a literal ${VAR} in
the output (docker-compose convention). Negative lookbehind in the match
regex prevents substitution when preceded by $.
- m3 (MINOR — documentation): docs expanded with a 3-row syntax comparison
table explaining string-safe vs raw-injection modes.
- m4 (MINOR — tests): added 3 edge-case tests covering JSON injection
attempt, multiline/backslash values, and the new $${VAR} escape hatch.
- n5 (NIT — stale tip): TUI tip at tips.tsx:150 now mentions both
${VAR} and {env:VAR} syntaxes.
Deferred (larger refactor, scope discipline):
- M1 (comment handling): substitutions still run inside // comments. Same
pre-existing behavior for {env:VAR}. Would require unified substitution
pass. Can be a follow-up PR.
* feat: add ${VAR:-default} syntax for fallback values
Extends the ${VAR} substitution to support POSIX/docker-compose-style
defaults. Matches user expectations from dotenv, docker-compose, and
shell: default value is used when the variable is unset OR empty
(matching :- semantics rather than bare -).
- ${VAR:-default} uses 'default' when VAR is unset or empty string
- ${VAR:-} (empty default) resolves to empty string
- Defaults with spaces/special chars supported (${VAR:-Hello World})
- Default values are JSON-escaped (same security properties as ${VAR})
- $${VAR:-default} escape hatch works for literal preservation
Per research, ${VAR:-default} is the de facto standard across
docker-compose, dotenv, POSIX shell, GitHub Actions, and Terraform —
users arrive from these tools expecting this syntax.
Added 7 tests covering unset/set/empty cases, empty default, spaces,
JSON-injection attempt, and escape hatch.
* feat: add config_env_interpolation telemetry event
Collect signals on env-var interpolation usage to detect footguns and
guide future improvements. Tracks per config-load:
- dollar_refs: count of ${VAR} / ${VAR:-default} references
- dollar_unresolved: ${VAR} with no value and no default → empty string
(signal: users writing fragile configs without defaults)
- dollar_defaulted: ${VAR:-default} where the fallback was used
(signal: whether defaults syntax is actually being used)
- dollar_escaped: $${VAR} literal escapes used
- legacy_brace_refs: {env:VAR} references (raw-injection syntax)
- legacy_brace_unresolved: {env:VAR} with no value
Emitted via dynamic import to avoid circular dep with @/altimate/telemetry
(which imports @/config/config). Event fires only when interpolation
actually happens, so no-env-ref configs don't generate noise.
What this lets us answer after shipping:
- How many users hit 'unresolved' unintentionally? → consider failing loud
- Is ${VAR:-default} getting adopted? → iterate on docs if not
- Are users still writing the legacy {env:VAR}? → plan deprecation
- Is the $${VAR} escape rare? → simplify docs if so
Adds event type to ALL_EVENT_TYPES completeness list (43 → 44).
* fix: address P1 double-substitution from cubic/coderabbit review
Both bots flagged a backward-compat regression: ${VAR} pass runs after
{env:VAR} pass, so an env value containing literal ${X} got expanded in
the second pass. Reordering only fixed one direction (reverse cascade
still possible when ${VAR}'s output contains {env:Y}).
Correct fix: single-pass substitution with one regex alternation that
evaluates all three patterns against the ORIGINAL text. Output of any
pattern cannot be re-matched by another.
Single regex handles:
$${VAR} or $${VAR:-default} → literal escape
(?<!$)${VAR}[:-default] → JSON-safe substitution
{env:VAR} → raw text injection
Regression tests added for both cascade directions:
- env.A="${B}" + {env:A} → literal ${B} stays
- env.A="{env:B}" + ${A} → literal {env:B} stays
All 34 tests pass. Typecheck + marker guard clean.
If the variable is not set and no default is given, it resolves to an empty string. Bare `$VAR` (without braces) is **not** interpolated — use `${VAR}` or `{env:VAR}`.
35
+
36
+
**Why two syntaxes?**`${VAR}` JSON-escapes the value so tokens containing quotes or braces can't break the config structure — the safe default for secrets. `{env:VAR}` does raw text injection for the rare case where you need to inject numbers or structure into unquoted JSON positions.
0 commit comments