This is the full developer documentation for varlock # About @env-spec > Understanding the env-spec specification and how varlock implements it @env-spec is a DSL that extends normal `.env` syntax. It allows adding structured metadata using `@decorator` style comments (similar to [JSDoc](https://jsdoc.app/)) and a syntax for setting values via explicit function calls. This lets us express a declarative schema of our environment variables in a familiar format, not tied to any specific programming language or framework. ### A short example: [Section titled “A short example:”](#a-short-example) .env.schema ```env-spec # Stripe secret api key # @required @sensitive @type=string(startsWith="sk_") # @docs(https://docs.stripe.com/keys) STRIPE_SECRET_KEY=encrypted("asdfqwerqwe2374298374lksdjflksdjf981273948okjdfksdl") ``` ### Why is this useful? [Section titled “Why is this useful?”](#why-is-this-useful) Loading a schema file full of structured metadata gives us: * additional validation, coercion, type-safety for your env vars * extra guard-rails around handling of `@sensitive` data * more flexible loading logic without hand-rolled application code or config files * a place to store default values, clearly differentiated from placeholders This schema information is most valuable when it is **shared across team members** and machines. So in most cases, this means creating a git-committed `.env.schema` file, instead of the familiar `.env.example` file used by many projects. The difference is that now the schema can be used on an ongoing basis, instead of just once to create an untracked local copy. Building on this, you could use additional files which set values. They could add additional items or override properties of existing ones. Whether you want to use a single git-ignored `.env` file, or apply a cascade of environment-specific files (e.g., `.env`, `.env.local`, `.env.test`, etc) is up to you. However the new ability to use function calls to safely decrypt data, or load values from external sources, means you’ll likely be tempted to use git-committed `.env` files much more. An env-spec enabled tool would load all env files appropriately, merging together both schema and values, as well as additional values read from the shell/process. Then the schema would be applied which could transform and fill values, for example decrypting or fetching from an external source, as well as applying coercion and validation. Backwards compatibility This is designed to be mostly backwards compatible with traditional .env files. However, as there is no standard .env spec and various tools have different rules and features, we made some decisions to try to standardize things. Our tools may support additional compatibility flags if users want to opt in/out of specific behaviours that match other legacy tools. The extended feature set means an env-spec enabled parser will successfully parse env files that other tools may not. ### What is included in env-spec? [Section titled “What is included in env-spec?”](#what-is-included-in-env-spec) This package defines a parser and related tools for parsing an @env-spec enabled .env file. It does not provide anything past this parsing step, such as actually loading environment variables. ### Why did we create this? [Section titled “Why did we create this?”](#why-did-we-create-this) We previously created DMNO and saw immense value in this schema-driven approach to configuration. With env-spec, we wanted to provide a standard that could benefit anyone who uses .env files (and even those who don’t!). There’s an incredible ecosystem of libraries and tools that have adopted .env, and we want to make it easier for everyone to benefit from additional guardrails, with as little upfront work as possible. We’ve also seen the explosion of AI-assisted coding tools which means that users are even more likely to leak sensitive configuration items, like API keys. If we can help to improve the security posture for these users, then hopefully that improves things for everyone. How can I help? If you’re a maintainer, author, contributor, or an opinionated user of tools that rely on .env files, please read through our RFC. We are not trying to build in a vacuum and we want your input. We’d also love your feedback on varlock which is built on top of @env-spec since it provides (we hope!) a solid reference implementation. *If this resonates with you, please reach out. We welcome your feedback and we welcome additional contributors.* *** # @env-spec Reference > Reference docs and details for @env-spec Tip In this spec, we don’t make any assumptions about the meaning of specific decorators, or function calls. This document just deals with how the syntax itself is parsed and structured. ## Config Items [Section titled “Config Items”](#config-items) Config items define individual env vars. * Each has a key, an optional value, and optional attached comments * Keys must start with `[a-ZA-Z_]`, followed by any of `[a-ZA-Z0-9_]` — ✅ `SOME_ITEM`, ❌ `BAD-KEY`, ❌ `2BAD_KEY` * Setting no value is allowed and will be treated as `undefined` — `UNDEF_VAR=` * note that no value (`ITEM=`) is treated slightly differently than an explicit `ITEM=undefined` when combining multiple definitions * An explicit empty string is allowed — `EMPTY_STRING_VAR=""` * Single-line values may be wrapped in quotes or not, and will follow the common value-handling rules (see below) * Multi-line string values may be wrapped in either `( ' | " | """ | ``` )` - but we **strongly** recommend using triple backticks only for consistency ````env-spec NO_VALUE= EXPLICIT_UNDEFINED=undefined EMPTY_STRING="" UNQUOTED=asdf QUOTED="asdf" FUNCTION_CALL=fn(foo, "bar") MULTILINE_STRING=``` multiple lines ``` ```` ## Comments and @decorators [Section titled “Comments and @decorators”](#comments-and-decorators) Comments in env-spec (like traditional .env files) start with a `#`. Unlike traditional .env files, comments may contain additional metadata by using `@decorators`, which may be attached to specific config items, sections, or the entire document. * Comments can be either on their own line, or at the end of a line after something else * Leading whitespace after the `#` is optional, but a single space is recommended * If a comment line starts with a @decorator, it will be considered a *decorator comment line* * Otherwise it is a *regular comment line* and any contained @decorators will be ignored * A decorator comment line may contain multiple decorators * A decorator comment line may end with an additional comment, in which decorators will be ignored * A post-value comment may also contain decorators, but is not recommended ```env-spec # ❌ leading space makes this invalid # this is a regular comment line # @dec2 @dec2=foo # this is a decorator comment line FOO=val # this is a post-value comment BAR=val # @dec # post-value comments may also contain decorators # regular comment lines @ignore contained @decorators # @dec # as are @decorators within an extra comment after a decorator comment line BAZ= # @dec # this is @ignored too ``` ### Decorators [Section titled “Decorators”](#decorators) Decorators are used within comments to attach structured data to specific config items, or within a standalone comment block to alter a group of items or the entire document and loading process. * Each decorator has a name and optional value (`@name=value`) or is a bare function call `@func()` * The two forms carry a meaning: **bare function calls `@func(...)` may be used multiple times** (their args accumulate, e.g. multiple `@docs(...)` links), while **decorators with a value `@name=value` are single-use** * A decorator value may itself be a function call (`@name=func(args)`), an object literal (`@name={key=value}`), or an array literal (`@name=[a, b, c]`). These are all single-use values, not bare function calls * Using the name only is equivalent to setting the value to true — `@required` === `@required=true` * Multiple decorators may be specified on the same line * Decorator values will be parsed using the common value-handling rules (see below) .env.schema ```env-spec # @willBeTrue @willBeFalse=false @explicitTrue=true @undef=undefined @trueString="true" # @int=123 @float=123.456 @willBeString=123.456.789 # @doubleQuoted="with spaces" @singleQuote='hi' @backTickQuote=`hi` # @unquoted=this-works-too @withNewline="new\nline" # @funcCallNoArgs=func() @dec=funcCallArray(val1, "val2") @dec=funcCallObj(k1=v1, k2="v2") # @objectLiteral={k1=v1, k2="v2"} @arrayLiteral=[a, b, c] # standalone object/array values # @anotherOne # and some comments, this @decorator is ignored # this is a comment and this @decorator is ignored ``` ### Dividers [Section titled “Dividers”](#dividers) A divider is a comment that serves as a separator, like a `
` in HTML. * A comment starting with `---` or `===` is considered a divider — `# ---`, `# ===` * A *single* leading whitespace is optional (but recommended — `# ---`, `#---` ) * Anything after that is ignored and valid — `# --- some info`, `# ------------` ```env-spec # a divider can be used to visually separate sections # --- ITEM1= ITEM2= # --- another divider --- ITEM3= ``` ### Config Item Comments [Section titled “Config Item Comments”](#config-item-comments) Comment lines directly preceding an item will be attached to that item, along with the decorators contained within. * A blank line or a divider will break the above comments from being attached to the item below * Both decorator and regular comment lines may be interspersed * Standalone comments only count as decorator comments when the comment content starts with `@` * Post-value comments may also contain decorators, but should be used sparingly ```env-spec # these comments are attached to ITEM1 below # @dec1 @dec2 # meaning these decorators will affect the item # additional comments can be interspersed with decorators ITEM1= # @dec3 # and a post-value comment can be used too # not attached due to blank line # also not attached due to divider # --- ITEM2= ``` ### Comment blocks & document header [Section titled “Comment blocks & document header”](#comment-blocks--document-header) A comment block is a group of continuous comments that is not attached to a specific config item. * The comment block is ended by an empty line, a divider, or the end of the file * All comment blocks before the first config item in a document are considered part of the same *document header* until one of those endings appears * Decorators from this header can be used to configure all contained elements, or the loading process itself * We recommend ending the header with a divider for clarity, but `# ---` is optional ```env-spec # this is the document header and usually contains root decorators # which affect default settings and the behavior of the tool that will be parsing this file # @dec1 @dec2 # --- # this is another comment block # and is not attached to an item # this comment is attached to the item below ITEM1= ``` ## Common rules [Section titled “Common rules”](#common-rules) ### Value handling [Section titled “Value handling”](#value-handling) Values are interpreted similarly for config item values, decorator values, and values within function call arguments. Values may be wrapped in quotes or not, but handling varies slightly: #### Unquoted values [Section titled “Unquoted values”](#unquoted-values) * Will coerce `true`, `false`, `undefined` — `@foo=false` * Will coerce numeric values — `@int=123 @float=123.456` * if the number is too large, would lose precision, or would change formatting, it will remain a string * May be interpreted as a function call (see below) * Otherwise will be treated as a string * May not contain other characters depending on the context: * config item values - may not contain `#` * decorator values - may not contain `[ #]` * function call arg values - may not contain `[),]` #### Quoted values [Section titled “Quoted values”](#quoted-values) * A value in quotes is *always* treated as a string — `@d1="with spaces"`, `@trueString="true"`, `@numStr="123"` * All quote styles ``[`'"]`` are ok — ``@dq="c" @bt=`b` @sq='a'`` * Escaped quotes matching the wrapping quote style are ok — `@ok="escaped\"quote"` * Single quote wrapped strings do not support [expansion (see below)](#expansion) * In `"` or `` ` `` wrapped values, the string `\n` will be converted to an actual newline * Multi-line strings may be wrapped in `(```|"""|"|')` * only available for config item values, not decorators or within function args ### Regex literals [Section titled “Regex literals”](#regex-literals) Regex literals use JavaScript-style `/pattern/flags` syntax and can be used anywhere a value is expected, for example as function arguments or decorator option values. * The pattern is delimited by `/` characters * Forward slashes within the pattern must be escaped as `\/` * Optional flags (`g`, `i`, `m`, `s`, `u`, `y`) may follow the closing `/` ```env-spec ITEM=fn(/^dev.*/i, dev, "production", prod) # @type=string(matches=/^sk-[a-zA-Z0-9]+$/) API_KEY= ``` ### Function calls [Section titled “Function calls”](#function-calls) Function calls may be used for item values `ITEM=fn()`, decorator values `# @dec=fn()`, and bare decorator functions `# @func()`. In each case, much of the handling is the same. * a value must not be wrapped in quotes to be interpreted as a function call * function names must start with a letter, and can then contain letters, numbers, and underscores `/[a-ZA-Z][a-ZA-Z0-9_]*/` * you can pass no args, a single arg, or multiple args * you may also pass key value pairs at the end of the list * each value will be interpreted using common value-handling rules (see above) ```env-spec NO_ARGS=fn() SINGLE_ARG=fn(asdf) MULTIPLE_ARGS=fn(one, "two", three, 123.456) KEY_VALUE_ARGS=fn(key1=v1, key2="v2", key3=true) MIXED_ARGS=fn(item1, item2, key1=v1, key2="v2", key3=true) NOT_FN_CALL="fn()" # treated as string ``` ### Object & array literals [Section titled “Object & array literals”](#object--array-literals) Standalone objects and arrays use distinct brackets, keeping them unambiguous from function calls (which use `()`): * `{ key=value, ... }` is an **object**, where keys use `=` (as elsewhere in the spec), not `:` * `[ value, ... ]` is an **array** * they may be nested, and used as decorator values or as function-call arguments * they are parsed using the common value-handling rules, and may span multiple lines (see [Multi-line literals](#multi-line-literals) below) * they are **not yet supported as standalone config item values** (e.g. `ITEM={...}`). For now an item value that starts with `{`/`[` is still treated as a string ```env-spec # @dec={ key1=v1, key2="v2", nested={ a=1 } } # @dec=[a, b, c] # @dec=fn(opts={ retries=3 }, tags=[x, y]) ITEM= ``` This is why per-item options are written `@sensitive={preventLeaks=false}` rather than `@sensitive(...)`. The latter (a bare function call) is reserved for repeatable decorators. #### When to reach for a literal (and when not to) [Section titled “When to reach for a literal (and when not to)”](#when-to-reach-for-a-literal-and-when-not-to) Object/array literals earn their place only where there is **no function-call `()` already carrying the structure**. In practice that means two situations: 1. **The value of a non-function decorator**, e.g. `@sensitive={preventLeaks=false}`. These decorators can’t take bare `()` args (that syntax is reserved for repeatable decorators), so an object value is the only way to attach options. 2. **A single argument that is itself a nested object or list**, sitting alongside other args, e.g. `fn(retry={count=3, backoff=2})`. Do **not** wrap a function’s (or function-decorator’s) own named arguments in a literal. The parentheses already *are* the options bag. The following are correct as written, and the literal-wrapped versions are redundant: | Prefer | Avoid | | ----------------------------------------- | ------------------------------------------- | | `@type=string(minLength=3, maxLength=50)` | `@type=string({minLength=3, maxLength=50})` | | `@generateTsTypes(path=env.d.ts)` | `@generateTsTypes({path=env.d.ts})` | | `cache(fn, ttl=1h, key=k)` | `cache(fn, {ttl=1h, key=k})` | Also prefer the existing positional/comma forms for: * **ordered pairs**: `ifs(cond1, val1, cond2, val2, default)` and `remap($VAR, match1, result1, ...)` read like a spreadsheet `IFS()`/lookup and stay flat. Crucially, `remap` match keys may be **regexes or non-identifier strings**, which cannot be object keys (keys must match `/[a-zA-Z][a-zA-Z0-9_]*/`), so an object would be strictly less expressive. * **value lists**: `@type=enum(dev, staging, prod)` already reads as a list; `enum([...])` only adds brackets. #### Multi-line literals [Section titled “Multi-line literals”](#multi-line-literals) Object and array literals may span multiple lines, which is handy for long key lists (e.g. picking many keys to import). The continuation convention matches function calls and depends on where the literal lives: * **Inside a decorator** (which lives in a `#` comment), every continuation line must start with `#`: ```env-spec # @import(./.env.shared, pick=[ # DATABASE_URL, # REDIS_URL, # STRIPE_KEY, # ]) DATABASE_URL= ``` ```env-spec # @sensitive={ # preventLeaks=false, # } SECRET= ``` * **On an item value line**, where the literal is nested in a function call, plain newlines are used (no `#`): ```env-spec ALLOWED=fn([ dev, staging, prod, ]) ``` A trailing comma is allowed. If a `#`-prefixed continuation is omitted in a decorator, the bracket simply falls back to a plain string value and the following lines remain independent config items. They are never silently absorbed into the literal. Within a multi-line literal (or function call), `#` introduces a comment that runs to the end of the line, so entries can be commented out or annotated. This applies the same way to multi-line function-call arguments. ```env-spec # @import(./.env.shared, pick=[ # DATABASE_URL, # # REDIS_URL, ← commented out, skipped # STRIPE_KEY, # primary payment key # ]) DATABASE_URL= ``` ### String expansion [Section titled “String expansion”](#expansion) While the parser itself does not include any implemention of specific functions, it does handle *expansion* of strings - and it uses several function calls under the hood to do so. This means a few basic function calls, while not implemented, have specific inherent meaning and must be implemented similarly across all tools that support this spec. Expansion can be used within item values, decorator values, and function call arguments. *Note that single quote wrapped strings are NOT expanded.* * `$ITEM_NAME` -> `ref(ITEM_NAME)` * `${ITEM_NAME}` -> `ref(ITEM_NAME)` * `pre${ITEM_NAME}post` -> `concat("pre", ref(ITEM_NAME), "post")` * `${ITEM_NAME:-defaultval}` -> `fallback(ref(ITEM_NAME), "defaultval")` * `${ITEM_NAME-defaultval}` -> `fallback(ref(ITEM_NAME), "defaultval")` * `$(my-cli arg --arg2)` -> `exec("my-cli arg --arg2")` Keep it simple We recommend using expansion only for simple refs `$ITEM`/`${ITEM}` and skipping the rest. * Use bracketed version within a larger string - `fn("${ENV}_db")` * Skip the brackets otherwise - `fn($ENV)` The rest is implemented to match other popular tools, but we do not recommend using them, as intent can be more clearly expressed using function calls directly. # @env-spec VS Code extension > Syntax highlighting and tooling for @env-spec enabled .env files The @env-spec VS Code and Open VSX extensions provide language support for @env-spec enabled .env files. ## Installation [Section titled “Installation”](#installation) The extension is available on the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=varlock.env-spec-language) and [Open VSX Registry](https://open-vsx.org/extension/varlock/env-spec-language) for those who use VS Code forks like Cursor and Windsurf. ## Features [Section titled “Features”](#features) * Syntax highlighting * IntelliSense for decorators, `@type` values, type options, resolver functions, and `$KEY` references * Enum value completion for item values below `@type=enum(...)` * Inline validation for invalid enum values, incompatible decorators, and obvious static `@type` mismatches * Hover info for common @decorators * Comment continuation - automatically continue comment blocks when you hit enter within one ## IntelliSense and diagnostics [Section titled “IntelliSense and diagnostics”](#intellisense-and-diagnostics) ### Decorators and built-in types [Section titled “Decorators and built-in types”](#decorators-and-built-in-types) The extension suggests decorators and built-in `@type=` values directly inside comment blocks. ![Decorator and type completion](/env-spec-vscode/autocomplete.gif) ### Type option completions [Section titled “Type option completions”](#type-option-completions) Built-in types surface context-aware option completions like `email(normalize=...)`, `ip(version=..., normalize=...)`, and `url(prependHttps=...)`. ![Type option completion](/env-spec-vscode/types.gif) ### Email-specific option completions [Section titled “Email-specific option completions”](#email-specific-option-completions) Type-specific completions also work for focused cases like `email(normalize=...)`, with boolean choice values suggested inline. ![Email option completion](/env-spec-vscode/email.gif) ### Enum value completions [Section titled “Enum value completions”](#enum-value-completions) When an item is declared as `@type=enum(...)`, the allowed values are suggested directly on the item value line below. ![Enum value completion](/env-spec-vscode/enum.gif) ### Variable references [Section titled “Variable references”](#variable-references) Typing `$` inside values and decorator expressions suggests config keys from the current file. ![Key reference completion](/env-spec-vscode/key.gif) ### Prefix-aware completions [Section titled “Prefix-aware completions”](#prefix-aware-completions) Decorator and validation workflows also support prefix-related configuration scenarios while editing schema comments. ![Prefix-related completion or validation](/env-spec-vscode/prefix.gif) ### Invalid decorator combinations [Section titled “Invalid decorator combinations”](#invalid-decorator-combinations) Autocomplete filters out incompatible decorators like `@required` and `@optional`, and inline diagnostics catch invalid combinations if they still appear in the file. ![Incompatible decorator diagnostics](/env-spec-vscode/exclusive_options.gif) ### Inline validation [Section titled “Inline validation”](#inline-validation) The extension also highlights obvious static validation issues, such as invalid enum values or incorrect `prependHttps` URL usage. ![Inline validation](/env-spec-vscode/prepend_https.gif) ## How to use this extension [Section titled “How to use this extension”](#how-to-use-this-extension) The new @env-spec language mode should be enabled automatically for any .env and .env.\* files, but you can always set it via the Language Mode selector in the bottom right of your editor. # Installation > How to install and set up Varlock in your project There are two ways to install Varlock: 1. Install as a `package.json` dependency in JavaScript/TypeScript projects (the `varlock` CLI package) 2. Install as a standalone binary If you prefer to let your AI agent install Varlock for you, you can skip these instructions and build a prompt for your agent: [Open Prompt Builder](/prompt-builder/) ### Docs MCP [Section titled “Docs MCP”](#docs-mcp) There is also a Docs MCP server that exposes a search tool. See more details [here](/guides/mcp/#docs-mcp). ## As a JavaScript/TypeScript dependency [Section titled “As a JavaScript/TypeScript dependency”](#as-a-javascripttypescript-dependency) Requires: * Node.js version 22 or higher ### Installation [Section titled “Installation”](#installation) To install `varlock` in your project, run: * npm ```bash npx varlock init ``` * pnpm ```bash pnpm dlx varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx varlock init ``` * yarn ```bash yarn dlx varlock init ``` This will install `varlock` as a dependency and scan your project for `.env` files and create a `.env.schema` file in the root of your project. Depending on your project configuration, it will optionally: * Remove your existing `.env.example` file * Add decorators to your `.env.schema` file to specify the type of each environment variable For AI agent workflows, use non-interactive mode instead: * npm ```bash npx varlock init --agent ``` * pnpm ```bash pnpm dlx varlock init --agent ``` * bun ```bash bunx varlock init --agent ``` * vlt ```bash vlx varlock init --agent ``` * yarn ```bash yarn dlx varlock init --agent ``` ## As a standalone binary [Section titled “As a standalone binary”](#as-a-standalone-binary) To install `varlock` CLI as a binary, run: ```bash # Install via homebrew brew install dmno-dev/tap/varlock # OR via cURL curl -sSfL https://varlock.dev/install.sh | sh -s ``` Then run the setup wizard to help you get started: ```bash varlock init ``` Windows Defender If Windows flags `varlock-local-encrypt.exe`, see the [local encryption guide, Windows Defender false positives](/guides/local-encryption/#windows-defender-false-positives) for verification and recovery steps. ## Varlock skill [Section titled “Varlock skill”](#varlock-skill) Then install the Varlock agent skill: * skills ```bash npx skills add dmno-dev/varlock ``` * GitHub CLI Requires GitHub CLI v2.90+. ```bash gh skill install dmno-dev/varlock varlock ``` See the [AI Tools guide](/guides/ai-tools/#install-the-varlock-skill) for update commands and agent-specific options. ## Editor and shell tooling [Section titled “Editor and shell tooling”](#editor-and-shell-tooling) Varlock ships with first-party tooling that makes authoring `.env.schema` files much easier. Set these up right after installing the CLI. ### VS Code extension [Section titled “VS Code extension”](#vs-code-extension) Install the [**@env-spec VS Code extension**](/env-spec/vs-code-ext/) from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=varlock.env-spec-language) or [Open VSX](https://open-vsx.org/extension/varlock/env-spec-language) (for Cursor, Windsurf, and other VS Code forks). It provides syntax highlighting, decorator and `@type` IntelliSense, `$KEY` reference completion, and inline validation while you edit `.env.schema` and other `@env-spec` files. ### Shell completion [Section titled “Shell completion”](#shell-completion) Enable tab completion for varlock commands and flags in bash, zsh, or fish. See the [Shell completion guide](/guides/shell-completion/) for one-time setup. Usually, adding `eval "$(varlock complete)"` to your shell profile is enough. # Introduction > Introduction to Varlock, the AI-safe env var toolkit for validating, securing, and sharing your environment variables Varlock is a universal configuration/secrets/environment variable management tool built on top of the [@env-spec](/env-spec/overview/) specification. It helps you manage, validate, and secure your environment configuration, with type-safe environment variables, multi-environment management, secure secret handling, and leak prevention. While it is written in TypeScript, it is language and framework agnostic, and meant to be used in any project that needs configuration at build or boot time, usually passed in via environment variables. ## Features [Section titled “Features”](#features) Varlock provides: * **[AI-Safe Config](/guides/ai-tools/)** - Your `.env.schema` gives AI agents full context on your config without ever exposing secret values. Prevent leaks to AI servers, and scan for leaked secrets with `varlock scan` * **[Security](/guides/secrets/)** - Automatic log redaction for sensitive values, leak detection in bundled code and server responses, and proactive scanning via `varlock scan` * **[Validation & Type Safety](/reference/data-types/)** - Validation with clear error messages, plus automatic type generation for IntelliSense support * **[Secure Secrets](/guides/secrets/)** - Built-in [device-local encryption](/guides/local-encryption/) with hardware-backed security (Secure Enclave, TPM), plus provider [plugins](/plugins/overview/) (e.g., [1Password](/plugins/1password/), [AWS](/plugins/aws-secrets/), [HashiCorp Vault](/plugins/hashicorp-vault/)) or any CLI tool using [exec()](/reference/functions/#exec) * **[Multi-Environment Management](/guides/environments/)** - Flexible environment handling with support for environment-specific files, local overrides, and value composition * **[Value Composition](/reference/functions/)** - Compose values together using functions, references, and external data sources * **[Framework Integrations](/integrations/overview/)** - Official integrations for Next.js, Vite, Astro, and more, plus support for any language via `varlock run` * **[Replacement for dotenv](/guides/migrate-from-dotenv/)** - Can be used as a direct replacement for `dotenv` in most projects with minimal code changes ## AI tooling [Section titled “AI tooling”](#ai-tooling) Varlock is built for AI-assisted development: your `.env.schema` gives agents schema context without exposing secret values. See the [AI Tools guide](/guides/ai-tools/) for `varlock run` with coding CLIs, the maintainer skill, [Docs MCP](/guides/mcp/#docs-mcp), and [LLMs.txt](https://varlock.dev/llms.txt). ## Next Steps [Section titled “Next Steps”](#next-steps) Ready to get started? Check out the [Installation](/getting-started/installation/) guide to set up Varlock in your project, or build a tailored setup prompt for your AI tool: [Open Prompt Builder](/prompt-builder/) # Migration > An overview if you have existing .env files and want to migrate to Varlock ## Loading env vars using Varlock [Section titled “Loading env vars using Varlock”](#loading-env-vars-using-varlock) ### Migration from dotenv (Node.js) [Section titled “Migration from dotenv (Node.js)”](#migration-from-dotenv-nodejs) In a [Node.js](/integrations/javascript/) app if you are already calling `dotenv/config`, you can replace it with `varlock/auto-load`. index.js ```diff -import 'dotenv/config'; +import 'varlock/auto-load'; ``` In some cases where `dotenv` is being called deep under the hood by another dependency, you may instead want to swap it in as a dependency override. See our [migrate from dotenv](/guides/migrate-from-dotenv/) guide for more information. ### Within a framework [Section titled “Within a framework”](#within-a-framework) We must replace the framework’s existing `.env` logic with Varlock. Our [framework integrations](/integrations/overview/) handle most of the work for you. After [installation](/getting-started/installation/), follow the instructions in the relevant integration guide to set up Varlock in your project. Usually this involves adding a new plugin to the existing build system or the framework config file. ### Minimal setup [Section titled “Minimal setup”](#minimal-setup) In some cases, a code-level integration may be challenging or impossible. In this case you can use [`varlock run`](/reference/cli-commands/#run) to boot your application with env vars injected from Varlock. For example `varlock run -- your-app`. Sometimes you may need to use this alongside a deeper integration, for example to feed env vars into external tools or additional scripts. ## Using `varlock/env` [Section titled “Using varlock/env”](#using-varlockenv) If you’re currently using `import.meta.env` or `process.env`, your code will still work after switching to Varlock. However, we recommend using varlock’s `ENV` object for better type-safety and an improved developer experience. index.js ```diff // Before (import.meta.env) -console.log(import.meta.env.SOMEVAR); // After (ENV) import { ENV } from 'varlock/env'; +console.log(ENV.SOMEVAR); ``` ![IntelliSense for ENV variables](/_astro/intellisense.l7SBUQg3_jY8ez.png) See our [integrations](/integrations/overview/) section for more information. # Usage > How to use Varlock in your project ## Basics [Section titled “Basics”](#basics) The basic workflow for using Varlock is to: 1. Run [`varlock init`](/reference/cli-commands/#init) to set up your `.env.schema` file 2. Run [`varlock load`](/reference/cli-commands/#load) to debug and refine your .env file(s) 3. Use Varlock to load, validate, and inject env vars into your application, either: * Use an [existing framework / tool integration](/integrations/overview/) that automatically calls Varlock under the hood (*recommended*) * Use `import 'varlock/auto-load'` in a backend JavaScript/TypeScript project * Boot your command via [`varlock run`](/reference/cli-commands/#run)\ (*necessary for non-JS/TS projects, or feeding env vars to external tools*) ## CLI Commands [Section titled “CLI Commands”](#cli-commands) ### `varlock load` [Section titled “varlock load”](#varlock-load) * npm ```bash npm exec -- varlock load ``` * pnpm ```bash pnpm exec -- varlock load ``` * bun ```bash bunx varlock load ``` * vlt ```bash vlx -- varlock load ``` * yarn ```bash yarn exec -- varlock load ``` * standalone binary ```bash varlock load ``` Validates your environment variables according to your `.env.schema` and associated `.env.*` files, and prints the results. Useful for debugging locally, and in CI to print out a summary of env vars, also when you’re authoring your `.env.schema` file and want immediate feedback. Tip Our [integrations](/integrations/overview) all use `varlock load` under the hood, so you’ll get the same developer experience, but typically they will only let you know if there are errors, rather than the full summary. See the [`varlock load` CLI Reference](/reference/cli-commands/#load) for more information. #### Understanding `varlock load` output [Section titled “Understanding varlock load output”](#understanding-varlock-load-output) By default, `varlock load` prints a human-readable summary to **stdout**. Schema and validation errors go to **stderr** first; if loading succeeds, you will see a `-- Resolved config --` header followed by one block per item. For example, given a schema like this: .env.schema ```env-spec # @defaultRequired=false # @defaultSensitive=false # --- # @required SERVICE_NAME=payments-api LOG_LEVEL=info # @type=number PORT="8080" # @type=boolean FEATURE_FLAG_BETA="true" # @required APP_ENV=development # @sensitive @required DATABASE_URL=postgres://user:pass@localhost:5432/app # @sensitive SESSION_SECRET=cache(randomHex(32), ttl="1h") # @sensitive @required STRIPE_SECRET_KEY= ``` `varlock load` prints (with `STRIPE_SECRET_KEY` set in `.env`, `APP_ENV` overridden in the shell, and `SESSION_SECRET` served from cache on a second run): ```ansi -- Resolved config -- ✅ SERVICE_NAME* └ "payments-api" ✅ LOG_LEVEL └ "info" ✅ PORT └ 8080 < coerced from "8080" ✅ FEATURE_FLAG_BETA └ true < coerced from "true" ✅ APP_ENV* └ "production" 🟡 process.env ✅ DATABASE_URL* 🔐sensitive └ po▒▒▒▒▒ ✅ SESSION_SECRET 🔐sensitive └ df▒▒▒▒▒ 📦 2m ago ✅ STRIPE_SECRET_KEY* 🔐sensitive └ sk▒▒▒▒▒ ``` Each item line shows: * A status icon (`✅` valid, or an error/warning icon if something failed) * The key name, with `*` if [`@required`](/reference/item-decorators/#required) * `🔐 sensitive` when the item is marked [`@sensitive`](/reference/item-decorators/#sensitive); values are redacted in the summary * The resolved value (quoted strings, colored booleans/numbers) * Inline hints such as `📦` (cached), `🟡 process.env` (overridden by the shell), or `< coerced from ...` when a value was coerced When validation fails, only failing items are shown unless you pass [`--show-all`](/reference/cli-commands/#load). Use [`--format json`](/reference/cli-commands/#load) or [`--format json-full`](/reference/cli-commands/#load) for machine-readable output. For example, in CI or when piping to other tools. For example, with `STRIPE_SECRET_KEY` left empty: ```ansi 🚨🚨🚨 Configuration is currently invalid 🚨🚨🚨 ❓ STRIPE_SECRET_KEY* 🔐sensitive └ undefined - Value is required but is currently empty 💥 Resolved config/env did not pass validation 💥 ``` ### `varlock run` [Section titled “varlock run”](#varlock-run) * npm ```bash npm exec -- varlock run -- ``` * pnpm ```bash pnpm exec -- varlock run -- ``` * bun ```bash bunx varlock run -- ``` * vlt ```bash vlx -- varlock run -- ``` * yarn ```bash yarn exec -- varlock run -- ``` * standalone binary ```bash varlock run -- ``` Executes a command in a child process, injecting your resolved and validated environment variables. This is useful when a code-level integration is not possible. For example, if you’re using a database migration tool, you can use `varlock run` to run the migration tool with the correct environment variables. Or if you’re using a non-js/ts language, you can use `varlock run` to run a command and inject validated environment variables. See the [`varlock run` CLI Reference](/reference/cli-commands/#run) for more information. ### `varlock encrypt` [Section titled “varlock encrypt”](#varlock-encrypt) * npm ```bash npm exec -- varlock encrypt --file .env.local ``` * pnpm ```bash pnpm exec -- varlock encrypt --file .env.local ``` * bun ```bash bunx varlock encrypt --file .env.local ``` * vlt ```bash vlx -- varlock encrypt --file .env.local ``` * yarn ```bash yarn exec -- varlock encrypt --file .env.local ``` * standalone binary ```bash varlock encrypt --file .env.local ``` Encrypts sensitive values using device-local encryption. Use `--file` to encrypt all `@sensitive` plaintext values in a `.env` file in-place, or run without arguments for interactive single-value encryption. Encrypted values are stored as `varlock("local:")` and are automatically decrypted during `varlock load` or `varlock run`. See the [`varlock encrypt` CLI Reference](/reference/cli-commands/#encrypt) and the [Local encryption guide](/guides/local-encryption/) for more information. ### `varlock reveal` [Section titled “varlock reveal”](#varlock-reveal) * npm ```bash npm exec -- varlock reveal ``` * pnpm ```bash pnpm exec -- varlock reveal ``` * bun ```bash bunx varlock reveal ``` * vlt ```bash vlx -- varlock reveal ``` * yarn ```bash yarn exec -- varlock reveal ``` * standalone binary ```bash varlock reveal ``` Securely view or copy decrypted values of `@sensitive` environment variables. Values are shown in an alternate screen buffer to prevent scrollback capture. See the [`varlock reveal` CLI Reference](/reference/cli-commands/#reveal) for more information. # Wrapping up > How to get your project ready for production and collaboration ## Next steps with your schema [Section titled “Next steps with your schema”](#next-steps-with-your-schema) With a more flexible env var toolkit, after an initial migration, you may be tempted to take advantage of Varlock’s features to improve your developer experience and security posture. * Move more configuration constants out of application code and into your `.env` files * Reduce the number of env-style checks in your code, favouring individual flags, with a default value set based on the current env * Add deeper validation, more thorough comments, and additional docs links to each env var within your schema * Compose values together to keep your configuration DRY * Use [imports](/guides/import/) to share common configuration across a monorepo, or to break up a large `.env.schema` * Reduce secret sprawl, by loading secrets from a single source of truth, instead of injecting them from your CI/hosting platform ## Repo setup [Section titled “Repo setup”](#repo-setup) ### `.gitignore` [Section titled “.gitignore”](#gitignore) Depending on your setup you will want to update your `.gitignore` to *not* ignore your `.env.schema` file and any other `.env.xxx` files that can now be safely committed to your repo if they don’t contain secrets (which they shouldn’t). If using [generated types](/reference/root-decorators/#code-generation), we also recommend that you ignore the generated file (usually `env.d.ts` in TypeScript) since it is regenerated automatically from your schema (on `varlock load`/`run`), like other build artifacts. .gitignore ```diff # Include .env.schema, .env. file # exclude local overrides .env.* .env.local .env.*.local # Exclude generated env types file env.d.ts ``` Tip Depending on the [AI Tools](/guides/ai-tools/) you use, you may need to add a similar rule to allow `.env.schema` to be modified. For example, in `.cursorignore`. ### Monorepos [Section titled “Monorepos”](#monorepos) Consider how you can reuse and modularize your schema if you have a monorepo or multi-service setup. Use [`@import()`](/guides/import/) to share config across packages, and see the [Monorepos](/guides/monorepos/) guide for layout patterns, Turborepo setup, incremental adoption, CI, and container builds. ## Deployment [Section titled “Deployment”](#deployment) ### CI/CD platforms [Section titled “CI/CD platforms”](#cicd-platforms) It may be useful to validate your schema in CI/CD pipelines, especially if you want to validate configurations that you don’t have access to locally (e.g. Staging or Production). You can do this manually by running `varlock load` in your pipeline. And if you’re using GitHub Actions, you can use the [Varlock GitHub Action](/integrations/github-action/) to validate your schema automatically. Tip Having a well architected multi-environment setup is key to healthy CI/CD workflows. See the [Environments](/guides/environments/) guide for more information. ### Production deployments [Section titled “Production deployments”](#production-deployments) Because varlock supports loading environment variables from the environment itself or via a [function](/reference/functions/) in your `.env.schema`, there are a few different approaches. If you’re already using your deployment platform’s environment variable management, you may not need to do anything to benefit from varlock’s validation and security features. If you have a multi-environment setup, you may need to set your environment flag (the item referenced by [`@currentEnv`](/reference/root-decorators/#currentenv), e.g. `APP_ENV`) to the correct environment. ```bash APP_ENV=production varlock run -- your-production-command ``` If you’re not using your deployment platform’s environment variable management, you may consider using one of our [plugins](/plugins/overview/) to securely load environment variables from a secret storage system such as [1Password](/plugins/1password/). For **container** deployments (Docker, Docker Compose, Kubernetes), see the [Docker guide](/guides/docker/). # AI Tools > Your schema gives AI agents full context, while your secrets never touch AI servers Your `.env.schema` gives AI agents full context on your configuration (variable names, types, validation rules, descriptions), while your secret values never leave your machine or touch AI servers. This solves two problems with AI-assisted development: 1. **Secret exposure**: AI tools read your project files, including `.env` files. With varlock, secrets are never stored in plain text. They’re fetched at runtime from secure providers. 2. **AI-generated leaks**: AI agents may hardcode secrets or log sensitive values in generated code. `varlock scan` catches these leaks before they’re committed, and runtime protection redacts secrets from logs and responses. ## Securely inject secrets into AI CLI tools [Section titled “Securely inject secrets into AI CLI tools”](#securely-inject-secrets-into-ai-cli-tools) Many AI coding assistants offer CLI tools that require API keys and other secrets. Instead of storing these secrets in plain text `.env` or `.json` files or exposing them in your shell history, use `varlock` to inject them securely at runtime. This applies both to config that might be required to bootstrap the tool itself, as well as things like [MCP servers](/guides/mcp/) that require API keys. ### 1. Install varlock [Section titled “1. Install varlock”](#1-install-varlock) If you haven’t already, [install varlock](/getting-started/installation/) on your system. ### 2. Create an environment schema [Section titled “2. Create an environment schema”](#2-create-an-environment-schema) Define your API keys and secrets in your `.env.schema` file. Mark sensitive values appropriately: .env.schema ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(allowAppAuth=true) # --- # @sensitive @required OPENAI_API_KEY=op(op://api-local/openai/api-key) # @sensitive @required ANTHROPIC_API_KEY=op(op://api-local/anthropic/api-key) # @sensitive @required GOOGLE_API_KEY=op(op://api-local/google/api-key) ``` Store the actual secret values in your preferred [secret provider](/plugins/overview/). The examples above use the [1Password plugin](/plugins/1password/)’s `op()` resolver, but any provider plugin works the same way ([AWS Secrets Manager](/plugins/aws-secrets/), [and more](/plugins/overview/)). If you’d rather not depend on an external provider, keep encrypted values in a gitignored `.env.local` using [device-local encryption](/guides/local-encryption/). See also the [Secrets guide](/guides/secrets/#loading-secrets-from-external-sources). Tip You can use any secrets provider you want, but we like 1Password since it uses biometric authentication for local access. All the examples in this guide use the [1Password plugin](/plugins/1password/) (`op()`) to show more real world examples. See that page for plugin setup and auth options. ### 3. Run your tool via `varlock run` [Section titled “3. Run your tool via varlock run”](#3-run-your-tool-via-varlock-run) Execute your AI CLI tool through `varlock` to securely inject environment variables: ```bash varlock run -- ``` See [AI CLI tool examples](#ai-cli-tool-examples) below for tool-specific setup. ## AI CLI tool examples [Section titled “AI CLI tool examples”](#ai-cli-tool-examples) Popular AI coding CLIs differ in which env vars they need and how they authenticate. Varlock supports two patterns: * **Project setup**: define secrets in the repo’s `.env.schema` (steps 1-2 above) and run `varlock run -- ` from the project directory. * **Personal setup**: keep a schema in your home directory (for example `~/.env.claude`) and pass it with [`-p`](/reference/cli-commands/#run) when launching a tool from any directory (usually using an alias). Varlock does not auto-load a global schema (that would make two developers’ machines behave differently for the same repo). * Claude [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) is Anthropic’s CLI tool for AI-assisted coding. **Environment variable:** `ANTHROPIC_API_KEY`. See [supported env variables](https://docs.claude.com/en/docs/claude-code/settings#environment-variables). **In a project**, add to `.env.schema`: ```env-spec # @sensitive @required ANTHROPIC_API_KEY=op(op://api-local/anthropic/api-key) ``` ```bash varlock run -- claude ``` **From any directory**, personal schema at `~/.env.claude`: \~/.env.claude ```env-spec # @sensitive @required ANTHROPIC_API_KEY=op(op://api-local/anthropic/api-key) ``` ```bash varlock run -p ~/.env.claude -- claude ``` **Shell alias** (optional): ```bash alias vclaude='varlock run -p ~/.env.claude --no-redact-stdout -- claude' ``` `--no-redact-stdout` keeps Claude’s own terminal output unredacted while secrets stay out of your shell history. * Opencode [Opencode](https://opencode.ai/) is a provider-agnostic AI coding assistant that works in your terminal. **Environment variables:** * `ANTHROPIC_API_KEY` for Claude models * `OPENAI_API_KEY` for OpenAI models * `OPENCODE_CONFIG` path to custom config file (optional) **Auth configuration:** run `opencode auth login` once. When prompted for an API key, paste an env reference instead: `{"env:ANTHROPIC_API_KEY"}` Your config file (`~/.local/share/opencode/auth.json`) should look like: \~/.local/share/opencode/auth.json ```json { "anthropic": { "type": "api", "key": "{env:ANTHROPIC_API_KEY}" } } ``` **In a project**, add to `.env.schema`: ```env-spec # @sensitive @required ANTHROPIC_API_KEY=op(op://api-local/anthropic/api-key) # @sensitive OPENAI_API_KEY=op(op://api-local/openai/api-key) ``` ```bash varlock run -- opencode # or with a specific model varlock run -- opencode --model claude-3-5-sonnet ``` **From any directory**, personal schema at `~/.env.opencode`: \~/.env.opencode ```env-spec # @sensitive @required ANTHROPIC_API_KEY=op(op://api-local/anthropic/api-key) # @sensitive OPENAI_API_KEY=op(op://api-local/openai/api-key) ``` ```bash varlock run -p ~/.env.opencode -- opencode # or with a specific model varlock run -p ~/.env.opencode -- opencode --model claude-3-5-sonnet ``` **Shell alias** (optional): ```bash alias vopencode='varlock run -p ~/.env.opencode --no-redact-stdout -- opencode' ``` See the [Opencode docs](https://opencode.ai/docs/) for more information. * Antigravity [Antigravity CLI](https://antigravity.google/docs/cli-getting-started) is Google’s agent-first terminal experience, the successor to [Gemini CLI](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/), which has been phased out. **Launch command:** `agy` **Authentication:** Antigravity signs in via Google OAuth on first launch (credentials live in your system keyring). ```bash varlock run -- agy ``` **From any directory**, personal schema at `~/.env.antigravity` for project-agnostic defaults: \~/.env.antigravity ```env-spec # @type=enum(development, staging, production) APP_ENV=development ``` ```bash varlock run -p ~/.env.antigravity -- agy ``` **Shell alias** (optional): ```bash alias vagy='varlock run -p ~/.env.antigravity --no-redact-stdout -- agy' ``` See the [Antigravity CLI getting started guide](https://antigravity.google/docs/cli-getting-started) and [installation & auth docs](https://antigravity.google/docs/cli-overview) for OAuth, enterprise, and headless setup. *** ## Install the Varlock skill [Section titled “Install the Varlock skill”](#install-the-varlock-skill) Give your AI agent project-scoped instructions for working with Varlock: security rules, schema checklists, and CLI guidance. Install using one of: * skills Install with the [skills CLI](https://github.com/vercel-labs/skills) (recommended): ```bash npx skills add dmno-dev/varlock ``` The CLI detects your installed agents and installs to the correct skills directory (Cursor, Claude Code, Codex, Copilot, OpenCode, and [50+ others](https://github.com/vercel-labs/skills#supported-agents)). Target a specific agent with `-a`: ```bash npx skills add dmno-dev/varlock -a cursor -a claude-code -y ``` Update later with: ```bash npx skills update varlock ``` * GitHub CLI Install with [`gh skill`](https://github.blog/changelog/2026-04-16-manage-agent-skills-with-github-cli/) (requires GitHub CLI v2.90+): ```bash gh skill install dmno-dev/varlock varlock ``` Target a specific agent and scope: ```bash gh skill install dmno-dev/varlock varlock --agent cursor --scope project ``` Update later with: ```bash gh skill update varlock ``` ## Agent mode [Section titled “Agent mode”](#agent-mode) Varlock provides a non-interactive `--agent` flag for `init` and `load`, designed for AI coding assistants running commands on your behalf. ### `varlock init --agent` [Section titled “varlock init --agent”](#varlock-init---agent) Use this when an AI agent is setting up Varlock in a project: ```bash varlock init --agent ``` In agent mode, init skips interactive confirmation prompts, uses deterministic defaults when multiple `.env.example` files are found, and prints schema review guidance for the agent to follow. ### `varlock load --agent` [Section titled “varlock load --agent”](#varlock-load---agent) Use this to validate resolved config without exposing secret values in logs or agent transcripts: ```bash varlock load --agent ``` Agent mode defaults to JSON output and redacts values marked `@sensitive`. It is not compatible with `--format env` or `--format shell` (which would expose raw values). Drop the `--agent` flag when you need human-readable output to show the user directly. ### Reading varlock output programmatically [Section titled “Reading varlock output programmatically”](#reading-varlock-output-programmatically) When an agent needs to consume varlock output rather than show it to a user, prefer machine-readable formats and branch on exit codes: * **`varlock load --agent`**: flat `{ "KEY": value }` JSON on stdout with `@sensitive` values redacted. Safe to keep in a transcript. * **`varlock load --agent --format json-full`**: the full serialized graph (sources, per-item validation state, sensitivity) with sensitive values redacted, for when you need to reason about *why* something is invalid, not just the values. Always keep `--agent` here: plain `--format json-full` emits **raw secret values** and must not go into an agent transcript. * **Exit codes**: `varlock load` exits non-zero when config is invalid, `varlock run` forwards the child command’s exit code, and `varlock scan` exits `1` when it finds a leaked secret. Check the code instead of parsing prose. * **stdout vs stderr**: pass `--summary-stderr` (or `--summary-file`) so the redacted human summary goes to stderr while stdout stays clean JSON for parsing. See the [CLI commands reference](/reference/cli-commands/#load) for the full output-format and exit-code details. Tip Use the [Prompt Builder](/prompt-builder/) to generate a tailored setup prompt for your stack and AI tool. ## Allowing schema files for AI tools [Section titled “Allowing schema files for AI tools”](#allowing-schema-files-for-ai-tools) Most AI tools ignore `.env.*` files by default. To ensure your AI tool can access your environment schema, add the following to your `.gitignore`: ```txt !.env.schema ``` If you use a tool with its own ignore file, check that tool’s documentation to see how it handles ignore files and make sure `.env.schema` is allowed. ## Custom instructions and rules [Section titled “Custom instructions and rules”](#custom-instructions-and-rules) The [Varlock agent skill](#install-the-varlock-skill) is the recommended way to give agents structured guidance for schema work and CLI usage. You can also provide broader context with the [full Varlock `llms.txt`](https://varlock.dev/llms-full.txt). In Cursor, this is accomplished via ‘Add New Custom Docs’. If your tool supports custom rules, you can use our own varlock [Cursor rule file from this repo](https://github.com/dmno-dev/varlock/blob/main/.cursor/rules/varlock.mdc) as a starting point to create your own that is most suited to your workflow. ## Scan for leaked secrets [Section titled “Scan for leaked secrets”](#scan-for-leaked-secrets) AI agents can sometimes hardcode secret values or leak them into generated code. Use `varlock scan` to detect leaked secrets in your codebase: ```bash # Scan the current directory for leaked secret values varlock scan # Scan specific paths varlock scan ./src ./config ``` You can also set up `varlock scan` as a git pre-commit hook to automatically catch leaks before they’re committed: ```bash # Add to your .git/hooks/pre-commit or use a hook manager like husky/lefthook varlock scan --staged ``` This is especially useful when working with AI coding tools. The scan command compares your resolved secret values against your codebase to find any that may have been accidentally included in plain text. ## Varlock Docs MCP [Section titled “Varlock Docs MCP”](#varlock-docs-mcp) We also have a docs MCP server that allows you to search the Varlock docs. See more details [here](/guides/mcp/#docs-mcp). # Caching > How varlock caching works, how to choose cache mode, and how to debug cache behavior Varlock has a built-in caching system, secured by [local encryption](/guides/local-encryption/). Rather than caching environment variable values, it is a general key-value store, usable by plugins to cache arbitrary internal requests, or by an explicit [`cache()`](/reference/functions/#cache) function to cache any resolver. ## Plugin cache behavior [Section titled “Plugin cache behavior”](#plugin-cache-behavior) Plugins have access to cache helpers that can be used to avoid repeated network round trips, or to hold short-lived tokens. They will usually expose params (like `cacheTtl`) to let the user opt into caching. This still respects global cache mode and CLI flags. Each plugin can decide how to cache things and it will often depend on the shape of the external API. `cacheTtl` can be set dynamically, for example `cacheTtl=forEnv(dev, "1h")` to cache only in development. Setting it to `false` (or an empty string) disables caching for that plugin instance. ## Common `cache()` use case: stable generated values [Section titled “Common cache() use case: stable generated values”](#common-cache-use-case-stable-generated-values) The most common use case for `cache()` is to hold [generated random values](/reference/functions/#random-value-generators) stable across restarts for local development. .env.local ```env-spec # Good for local-only generated values INSTANCE_ID=cache(randomUuid(), ttl=1d) # cache for 1 day TRACKING_ID=cache(randomUuid(), ttl=forever) # cache until manually cleared SESSION_SECRET=cache(randomHex(32)) # default is ttl=forever ``` This allows a new environment to generate values, while keeping them stable. ## TTL duration format (shared everywhere) [Section titled “TTL duration format (shared everywhere)”](#ttl-duration-format-shared-everywhere) Both plugins and `cache()` use the same duration format and parser for TTLs - values like `"30s"`, `"5m"`, `"1h"`, `"500ms"`, or `"2days"` - based on our [`duration` data type](/reference/data-types/#duration). The format is a number followed by a unit (`ms`, `s`, `m`, `h`, `d`, `w`), with long forms and plurals also accepted. For example, minute accepts any of `m`, `min`, `mins`, `minute`, `minutes`. Bare numbers are interpreted as milliseconds, and must be plain decimals (no hex or exponent notation). Use the keyword `forever` to mean “cache until manually cleared”. A TTL of `0` is rejected as ambiguous - use `forever` instead, or set `cacheTtl=false` to disable plugin caching. * `@initOp(..., cacheTtl=30m)` * `cache(someFunc(), ttl=1h)` * `cache(someFunc(), ttl=forever)` ## Cache modes [Section titled “Cache modes”](#cache-modes) Caching is mostly useful for local development, where there may be many invocations of varlock and strong [local encryption](/guides/local-encryption/) (e.g., biometric-protected keys) is available. The default mode (`auto`) picks the most secure persistent option available: 1. **disk** encrypted via local encryption - when a native encryption backend is available and not running in CI 2. **disk** encrypted with `_VARLOCK_CACHE_KEY` - when that env var is set (see below) 3. in-process **memory** caching otherwise (CI without a key, or the basic file-based encryption fallback) In memory mode, entries still avoid repeated work within a single invocation, but are not persisted across invocations. To explicitly set the cache mode (or disable caching entirely), use the [`@cache`](/reference/root-decorators/#cache) root decorator. Forcing `@cache=disk` in CI or while using the file-based encryption fallback is allowed, but emits a warning - with the file backend, the decryption key lives on the same disk as the cache, so encryption is obfuscation-only. ## Disk caching in CI with `_VARLOCK_CACHE_KEY` [Section titled “Disk caching in CI with \_VARLOCK\_CACHE\_KEY”](#disk-caching-in-ci-with-_varlock_cache_key) Memory mode doesn’t help when a CI job runs varlock in several separate processes. If the `_VARLOCK_CACHE_KEY` env var is set (a 256-bit hex key, typically provided as a CI secret), `auto` mode uses a **disk** cache encrypted with that key instead of falling back to memory. The key only ever lives in the environment - never on the runner’s disk - so the persisted cache file is genuinely encrypted. This is deliberately a separate variable from `_VARLOCK_ENV_KEY` (used for [encrypted deployments](/guides/encrypted-deployments/)) - that one is auto-generated and ephemeral in some flows, which would silently defeat caching. The key format is the same though, so you can generate one with `varlock generate-key`. Each key gets its own cache file (named by key fingerprint), so rotating the key naturally invalidates the old cache. `varlock cache` also operates on the active key’s cache file when `_VARLOCK_CACHE_KEY` is set. ## Disk cache location and security [Section titled “Disk cache location and security”](#disk-cache-location-and-security) The disk cache lives at `~/.config/varlock/cache/`, with one file per encryption key. A few things to keep in mind: * Only entry **values** are encrypted. Cache keys are stored in plaintext and include file paths, item names, and resolver source text. * The disk cache is per-OS-user and shared across all projects. This is intentional - projects often share config - but it means any env file you load can read and write it, so treat untrusted repos accordingly. * With the file-based encryption fallback, the decryption key sits on the same disk as the cache, so encryption is obfuscation-only (see warning above). ## Per-invocation CLI controls [Section titled “Per-invocation CLI controls”](#per-invocation-cli-controls) [`varlock load`](/reference/cli-commands/#load), [`varlock run`](/reference/cli-commands/#run), and [`varlock printenv`](/reference/cli-commands/#printenv) support: * `--skip-cache`: disables cache reads and writes for that invocation (overrides `@cache`) * `--clear-cache`: clears the active cache store before resolving values If both are used, the cache is cleared first, then reads and writes are skipped for the rest of the run. ## Inspecting and clearing cache [Section titled “Inspecting and clearing cache”](#inspecting-and-clearing-cache) Use [`varlock cache`](/reference/cli-commands/#cache) to inspect and clear the **disk cache**: ```bash varlock cache status varlock cache clear --yes varlock cache clear --plugin 1password --yes ``` Notes: * This command manages disk cache entries only. * In `memory` mode, entries are process-local and are not visible via `varlock cache`. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### ”I changed a resolver but still see old values” [Section titled “”I changed a resolver but still see old values””](#i-changed-a-resolver-but-still-see-old-values) * If you used a custom `key`, that key may intentionally pin the cache entry. * Run with `--clear-cache` or clear entries via `varlock cache clear`. ### ”`varlock cache` shows nothing, but caching seems active” [Section titled “”varlock cache shows nothing, but caching seems active””](#varlock-cache-shows-nothing-but-caching-seems-active) * You are likely in `memory` mode (for example in CI or file-backend fallback mode). ### ”I want no cache artifacts on disk” [Section titled “”I want no cache artifacts on disk””](#i-want-no-cache-artifacts-on-disk) * Use `@cache=memory` (or `@cache=disabled`) and avoid `disk` mode. # Code generation > Generate types and other code from your env schema, and extend it with plugins Your `.env.schema` is the single source of truth for your environment variables: their names, types, and metadata. Varlock can turn that schema into **generated code**: strongly-typed definitions for your language, and, via plugins, anything else you can derive from a schema. Generation is driven by per-language root decorators in your schema. Each one writes its own output file: .env.schema ```env-spec # @generateTsTypes(path=./env.d.ts) # @generatePythonEnv(path=./env_types.py) ``` Output is regenerated automatically on [`varlock load`](/reference/cli-commands/#load) and [`varlock run`](/reference/cli-commands/#run), or explicitly with [`varlock codegen`](/reference/cli-commands/#codegen). It’s derived only from non-environment-specific schema definitions, so the result is deterministic regardless of which environment is active. Tip `varlock init` detects your project’s primary language and adds the matching decorator for you. ## TypeScript [Section titled “TypeScript”](#typescript) [`@generateTsTypes`](/reference/root-decorators/#generatetstypes) is the default for JavaScript/TypeScript projects. It makes `import { ENV } from 'varlock/env'` fully typed, and augments `process.env` / `import.meta.env`: .env.schema ```env-spec # @generateTsTypes(path=./env.d.ts) ``` You can control what it emits: * **`exposeEnv`**: where the coerced `ENV` object lives: `global` (default, augments `varlock/env`), `local` (export a package-local `ENV` from the generated file, ideal for monorepos), or `none`. * **`processEnv`** / **`importMetaEnv`**: `strict` (only defined keys), `loose` (also allow extra keys), or `none` (don’t augment). See the [`@generateTsTypes` reference](/reference/root-decorators/#generatetstypes) for details. In monorepos where multiple packages have different schemas, prefer `exposeEnv=local` to avoid global augmentation clashes. See [Typed `ENV` across packages](/guides/monorepos/#typed-env-across-packages). ## Other languages [Section titled “Other languages”](#other-languages) Varlock ships generators for several languages. Each emits a **self-contained, idiomatic module**: a typed value object, a loader that reads the injected [`__VARLOCK_ENV`](/reference/reserved-variables/#__varlock_env) blob, and a `SENSITIVE_KEYS` constant (so you can build your own redaction or leak-scanning). No hand-rolled parsing required: | Language | Decorator | Loader | Guide | | -------- | -------------------- | ------------- | ------------------------------- | | Python | `@generatePythonEnv` | `load_env()` | [Python](/integrations/python/) | | Rust | `@generateRustEnv` | `env::load()` | [Rust](/integrations/rust/) | | Go | `@generateGoEnv` | `env.Load()` | [Go](/integrations/go/) | | PHP | `@generatePhpEnv` | `Env::load()` | [PHP](/integrations/php/) | For a language without a generated module, see [Other languages](/integrations/other-languages/). ## Disabling automatic generation [Section titled “Disabling automatic generation”](#disabling-automatic-generation) Set `auto=false` on any generator to skip it during `load`/`run`, then regenerate on demand with [`varlock codegen`](/reference/cli-commands/#codegen), useful for CI or a dedicated build step: .env.schema ```env-spec # @generateTsTypes(path=./env.d.ts, auto=false) ``` ## Extending with plugins [Section titled “Extending with plugins”](#extending-with-plugins) Code generation is contributed through a registry, and [plugins](/guides/plugins/) can register their own generators using the same mechanism the built-in ones use. A generator is a root decorator name plus a `generate` function that receives the resolved schema and returns file contents: my-plugin.ts ```ts import { plugin, type CodeGeneratorDef } from 'varlock/plugin-lib'; const generateZodSchema: CodeGeneratorDef = { decoratorName: 'generateZodSchema', generate: ({ fields, outputPath }) => { // `fields` is the language-agnostic resolved schema (keys, coerced/raw types, // required/sensitive flags, docs). Return the source to write to `outputPath`. const lines = fields.map((f) => ` ${f.key}: z.string(),`); return `import { z } from 'zod';\n\nexport const envSchema = z.object({\n${lines.join('\n')}\n});\n`; }, }; plugin.registerCodeGenerator(generateZodSchema); ``` Once registered, users trigger it from their schema like any other generator: .env.schema ```env-spec # @plugin(my-plugin) # @generateZodSchema(path=./env.schema.ts) ``` The `decoratorName` must start with `generate` (e.g. `generateZodSchema`), a shared convention that keeps code-generation decorators recognizable and separate from behavior decorators. Generators also get the common `path`, `auto`, and `executeWhenImported` args automatically, so your `generate` function only handles turning the schema into code. Plugins that register custom data types can also declare `coercedType` (what their `coerce()` function outputs: `'string'`, `'int'`, `'number'`, `'boolean'`, `'object'`, or `{ enum: [...] }`) so generated code types those fields correctly in every language: my-plugin.ts ```ts plugin.registerDataType({ name: 'retryCount', coercedType: 'int', coerce: (val) => parseInt(val, 10), }); ``` Data types that don’t declare a `coercedType` are treated as strings. # Docker > Using varlock with Docker containers for CI validation, runtime injection, multi-stage builds, and monorepo workarounds Varlock works well in containerized workflows, but **how** you use it depends on what you need at each stage of your pipeline. This guide covers the official [GHCR image](https://github.com/dmno-dev/varlock/pkgs/container/varlock), patterns for installing varlock yourself, and the pitfalls to avoid when secrets meet Docker layers. ## Choosing an approach [Section titled “Choosing an approach”](#choosing-an-approach) Before adding varlock to a Dockerfile, decide which role it plays: | Approach | When to use | varlock in production image? | | ---------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | **CI-only validation** | Validate schema and config in pipelines; runtime env comes from the platform | No. Run `varlock load` in CI only | | **Runtime injection** | Long-running containers (APIs, workers) that resolve secrets at boot via [plugins](/guides/plugins/) | Yes. Use `varlock run` as the entrypoint | | **Build-time** | SSR apps where a [framework integration](/integrations/overview/) injects resolved env into build output | Sometimes. Only in a **builder** stage, not the final runtime image | * CI-only validation Use varlock in your pipeline to catch schema errors before deploy. Your production container does not need the CLI. The hosting platform (or orchestrator) injects env vars, and varlock has already validated the config graph in CI. .github/workflows/ci.yml ```yaml - name: Validate environment schema run: | docker run --rm \ -v ${{ github.workspace }}:/work \ -w /work \ -e PWD=/work \ ghcr.io/dmno-dev/varlock:1.0.2 load ``` See also the [GitHub Action](/integrations/github-action/) for a native Actions integration. * Runtime injection Use when your container must resolve secrets at boot, for example via a [1Password service account token](/plugins/1password/), AWS IAM, or [OIDC workload identity](/guides/oidc/). The final image runs your app through `varlock run`: Dockerfile (runtime stage excerpt) ```dockerfile ENTRYPOINT ["varlock", "run", "--"] CMD ["node", "dist/server.js"] ``` Secrets are fetched when the container starts, not when the image is built. * Build-time Use when a [framework integration](/integrations/overview/) (Next.js, Vite SSR, Cloudflare Workers, etc.) resolves and injects env during `build`. Varlock belongs in the **builder** stage; the runtime image receives env via the platform or an encrypted blob, not via the CLI. For serverless SSR, prefer [encrypted deployments](/guides/encrypted-deployments/) over baking resolved values into image layers. ### When not to put varlock in your production image [Section titled “When not to put varlock in your production image”](#when-not-to-put-varlock-in-your-production-image) Don’t ship the CLI unless you need it at runtime Skip varlock in the final production image when: * **Serverless / edge**: integrations inject env at build or deploy time; the runtime has no filesystem or CLI access anyway * **Platform-managed env**: your host already injects env vars and you only want schema validation in CI * **Build-time-only resolution**: secrets are resolved during `docker build` and would end up in a layer (see [best practices](#best-practices) below) Adding varlock to every production image increases size and attack surface without benefit if secrets are already handled elsewhere. ## Official Docker image [Section titled “Official Docker image”](#official-docker-image) The image is published to GitHub Container Registry and built from the official release binaries on Alpine Linux: * **Image:** `ghcr.io/dmno-dev/varlock` * **Binary path:** `/usr/local/bin/varlock` * **Default workdir:** `/work` * **Entrypoint:** `varlock` (pass subcommands as arguments) ```bash # Pull a pinned version (recommended) docker pull ghcr.io/dmno-dev/varlock:1.6.0 # Run help docker run --rm ghcr.io/dmno-dev/varlock:1.6.0 --help # Validate schema in the current directory docker run --rm \ -v "$(pwd):/work" \ -w /work \ -e PWD=/work \ ghcr.io/dmno-dev/varlock:1.6.0 load ``` ### Available tags [Section titled “Available tags”](#available-tags) * `ghcr.io/dmno-dev/varlock:latest`: latest stable release * `ghcr.io/dmno-dev/varlock:1.6.0`: specific version (replace with the version you pin in CI/production) Pin image tags Use an explicit version tag in CI and production Dockerfiles. `latest` is convenient for local experiments but can change without notice. ## Best practices [Section titled “Best practices”](#best-practices) ### Never bake resolved secrets into image layers [Section titled “Never bake resolved secrets into image layers”](#never-bake-resolved-secrets-into-image-layers) Anything written during `RUN` is persisted in the image history. Avoid: Anti-pattern: secrets in layers ```dockerfile # BAD: resolved values become part of the image RUN varlock load > /app/.env.production RUN varlock run -- node scripts/build.js # if build embeds secrets in output ARG OP_SERVICE_ACCOUNT_TOKEN # build args are also stored in metadata ``` Instead: * Pass **secret-zero** credentials at **runtime** via environment variables or orchestrator secrets * Use **multi-stage builds** so builder-stage artifacts containing secrets are discarded * For SSR, use [@encryptInjectedEnv](/guides/encrypted-deployments/) so build output holds ciphertext, not plaintext ### Use the `/work` mount and `PWD=/work` pattern [Section titled “Use the /work mount and PWD=/work pattern”](#use-the-work-mount-and-pwdwork-pattern) The official image sets `WORKDIR /work`. When bind-mounting your project, mirror that layout and set `PWD` explicitly. Varlock uses the process working directory to resolve relative `@import()` paths and `.env` files: ```bash docker run --rm \ -v "$(pwd):/work" \ -w /work \ -e PWD=/work \ ghcr.io/dmno-dev/varlock:1.6.0 load ``` If your app lives in a subdirectory, mount that directory to `/work` (or adjust `-w` and `PWD` together). ### Pin versions [Section titled “Pin versions”](#pin-versions) Pin the GHCR image tag **and** any plugin versions in `.env.schema` when using the standalone binary (see [plugins in containers](#plugins-in-containers)). ## Multi-stage builds [Section titled “Multi-stage builds”](#multi-stage-builds) Copy the varlock binary from the official image into your application image. No need to install Node or curl in the final stage: Dockerfile ```dockerfile FROM ghcr.io/dmno-dev/varlock:1.6.0 AS varlock FROM node:22-alpine AS builder COPY --from=varlock /usr/local/bin/varlock /usr/local/bin/varlock WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile COPY . . # Validate schema during build (no secrets written to disk) RUN varlock load RUN pnpm build FROM node:22-alpine AS runner WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.env.schema ./.env.schema # Runtime: resolve secrets when the container starts COPY --from=varlock /usr/local/bin/varlock /usr/local/bin/varlock ENTRYPOINT ["varlock", "run", "--"] CMD ["node", "dist/server.js"] ``` Alpine runtime deps The official image includes `libstdc++` and `ca-certificates` required by the Bun-compiled binary. If you copy the binary into a minimal Alpine-based app image, add the same packages: ```dockerfile RUN apk add --no-cache ca-certificates libstdc++ ``` You can also use `COPY --from=ghcr.io/dmno-dev/varlock:1.6.0` directly without a named stage: ```dockerfile COPY --from=ghcr.io/dmno-dev/varlock:1.6.0 /usr/local/bin/varlock /usr/local/bin/varlock ``` ## Runtime: entrypoint and Docker Compose [Section titled “Runtime: entrypoint and Docker Compose”](#runtime-entrypoint-and-docker-compose) For containers that resolve secrets at boot, set varlock as the entrypoint and pass your process after `--`: Dockerfile ```dockerfile ENTRYPOINT ["varlock", "run", "--"] CMD ["node", "dist/server.js"] ``` Inject the environment flag and secret-zero credentials via your orchestrator, not via `docker build`: ```bash docker run --rm \ -e APP_ENV=production \ -e OP_SERVICE_ACCOUNT_TOKEN \ -v "$(pwd):/work" \ -w /work \ -e PWD=/work \ my-app:latest ``` ### Docker Compose [Section titled “Docker Compose”](#docker-compose) docker-compose.yml ```yaml services: app: build: . environment: APP_ENV: production OP_SERVICE_ACCOUNT_TOKEN: ${OP_SERVICE_ACCOUNT_TOKEN} entrypoint: ["varlock", "run", "--"] command: ["node", "dist/server.js"] volumes: - .:/work working_dir: /work ``` Compose secrets Use [Docker Compose secrets](https://docs.docker.com/compose/how-tos/use-secrets/) or an `.env` file excluded from git for tokens like `OP_SERVICE_ACCOUNT_TOKEN`. Never commit secret-zero values into `docker-compose.yml`. ## Monorepos and partial build context [Section titled “Monorepos and partial build context”](#monorepos-and-partial-build-context) In a monorepo, `.env.schema` often uses [`@import()`](/guides/import/) to pull shared config from other packages. For Docker builds, prefer importing **specific files** (optionally with `pick`) over whole directories. A [directory import](/guides/import/#directory) also pulls sibling `.env.local` / `.env.[currentEnv]` files, which you generally don’t want baked into an image and may not have copied into the build context: apps/web/.env.schema ```env-spec # @import(../../shared/.env.common) # @import(../api/.env.schema, pick=[DATABASE_URL, REDIS_URL]) ``` When Docker’s build context is limited to `apps/web/`, those imported files are **not** in the context, so `varlock load` fails with missing file errors. ### Workaround: copy imported files explicitly [Section titled “Workaround: copy imported files explicitly”](#workaround-copy-imported-files-explicitly) Until a dedicated **`varlock flatten`** command ships (to inline the import graph into a local directory for Docker builds), copy every file the import graph needs: Dockerfile: monorepo env files ```dockerfile # Build from repo root: docker build -f apps/web/Dockerfile . ARG SERVICE_DIR=web COPY .env.schema ./ COPY shared/.env.common ./shared/ COPY apps/${SERVICE_DIR}/.env.schema ./apps/${SERVICE_DIR}/ COPY apps/${SERVICE_DIR}/.env.production ./apps/${SERVICE_DIR}/ WORKDIR /app/apps/${SERVICE_DIR} RUN varlock load ``` Alternatively, set the build context to the **monorepo root** and use `docker build -f apps/web/Dockerfile .` so all import paths resolve naturally, at the cost of a larger context and slower builds. Coming soon A planned `varlock flatten` command will automate inlining imported env files for Docker builds. Until then, explicit `COPY` lines or a root build context are the supported approaches. ## Plugins in containers [Section titled “Plugins in containers”](#plugins-in-containers) Containers commonly authenticate to secret providers in two ways: 1. **Service account tokens**: pass a long-lived token (e.g. `OP_SERVICE_ACCOUNT_TOKEN`) as a runtime environment variable 2. **OIDC workload identity**: exchange a short-lived platform token for temporary credentials; no secret-zero in the image For OIDC on Fly.io, GCP Cloud Run, GitHub Actions, and other platforms, see the [OIDC workload identity guide](/guides/oidc/). When using the **standalone binary** (copied from GHCR or a release tarball), plugins must specify a **fixed version** in `@plugin()`. Semver ranges require a local `node_modules` install: .env.schema ```env-spec # @plugin(@varlock/1password-plugin@1.2.3) # @initOp(token=$OP_SERVICE_ACCOUNT_TOKEN) # --- OP_SERVICE_ACCOUNT_TOKEN= # @sensitive DATABASE_URL=op(op://prod/db/connection-string) # @sensitive ``` In **Node-based images** where varlock is an npm dependency, install plugins in `package.json` and reference them without a version (or with a range for validation only). See the [Plugins guide](/guides/plugins/#plugin-installation) for details. Plugin cache in CI/containers Ephemeral containers often run in memory cache mode. If your job invokes varlock multiple times and plugin fetches are slow, set [`_VARLOCK_CACHE_KEY`](/guides/caching/#disk-caching-in-ci-with-_varlock_cache_key) as a CI secret. See the [Caching guide](/guides/caching/) for details. ## Without the official image [Section titled “Without the official image”](#without-the-official-image) If you cannot pull from GHCR (air-gapped registry policy, custom base images, etc.), install varlock by another method: * npm (Node image) Best when your image already uses Node 22+ and varlock is a project dependency: Dockerfile ```dockerfile FROM node:22-alpine AS builder WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile COPY . . RUN pnpm exec varlock load RUN pnpm build ``` Install `varlock` and any plugins in `package.json`. The npm package includes the CLI. * install.sh Download a release binary during build (similar to local [installation](/getting-started/installation/#as-a-standalone-binary)): Dockerfile ```dockerfile FROM alpine:3.19 RUN apk add --no-cache curl ca-certificates libstdc++ \ && curl -sSfL https://varlock.dev/install.sh | sh -s -- --version=1.6.0 --dir=/usr/local/bin WORKDIR /work ENTRYPOINT ["/usr/local/bin/varlock"] ``` Pass `--version=x.y.z` for reproducible builds. For multi-arch images, prefer the release tarball tab, since `install.sh` picks the host architecture at install time. * Release tarball Matches how the [official Dockerfile](https://github.com/dmno-dev/varlock/blob/main/Dockerfile) is built. Download a versioned binary from GitHub releases: Dockerfile ```dockerfile FROM alpine:3.19 AS varlock-builder ARG VARLOCK_VERSION=1.6.0 ARG TARGETARCH RUN apk add --no-cache curl tar \ && ARCH="$([ "$TARGETARCH" = "arm64" ] && echo linux-musl-arm64 || echo linux-musl-x64)" \ && curl -L -o varlock.tar.gz \ "https://github.com/dmno-dev/varlock/releases/download/varlock@${VARLOCK_VERSION}/varlock-${ARCH}.tar.gz" \ && tar -xzf varlock.tar.gz \ && chmod +x varlock FROM alpine:3.19 RUN apk add --no-cache ca-certificates libstdc++ COPY --from=varlock-builder /varlock /usr/local/bin/varlock WORKDIR /work ENTRYPOINT ["/usr/local/bin/varlock"] ``` Build with Buildx for multi-platform support (`TARGETARCH` is set automatically). ## Building the image locally [Section titled “Building the image locally”](#building-the-image-locally) The repository root [Dockerfile](https://github.com/dmno-dev/varlock/blob/main/Dockerfile) builds the GHCR image. To build locally: ```bash # Pin a specific varlock version docker build --build-arg VARLOCK_VERSION=1.6.0 -t varlock:local . # Build using the latest GitHub release docker build --build-arg VARLOCK_VERSION=latest -t varlock:local . ``` ## When things go wrong [Section titled “When things go wrong”](#when-things-go-wrong) ### Permission denied on mounted volumes [Section titled “Permission denied on mounted volumes”](#permission-denied-on-mounted-volumes) Bind mounts inherit host ownership. If varlock cannot read `.env.schema` or write cache files: ```bash docker run --rm \ -u "$(id -u):$(id -g)" \ -v "$(pwd):/work" \ -w /work \ -e PWD=/work \ ghcr.io/dmno-dev/varlock:1.6.0 load ``` Or adjust host file permissions so the container user can read your env files. ### Network / plugin connectivity [Section titled “Network / plugin connectivity”](#network--plugin-connectivity) Plugins that call external APIs (1Password, AWS, Azure, Vault, etc.) need outbound network access from the container. If fetches fail with connection errors: * Confirm the container has DNS and egress to the provider’s endpoints * For local CLI tools (e.g. 1Password desktop app), use `--network host` only in local dev, not in production * Check that secret-zero tokens are passed at **runtime**, not only at build time ```bash # Local dev only: host network for desktop CLI integrations docker run --rm --network host \ -v "$(pwd):/work" \ -w /work \ -e PWD=/work \ ghcr.io/dmno-dev/varlock:1.6.0 load ``` For cloud deployments, prefer [service account tokens](/guides/plugins/) or [OIDC](/guides/oidc/) over desktop CLI integrations. ### Missing `@import` files [Section titled “Missing @import files”](#missing-import-files) See [Monorepos and partial build context](#monorepos-and-partial-build-context). The error usually lists a path outside your Docker build context. Add explicit `COPY` instructions or widen the context to the monorepo root. ### Wrong environment loaded [Section titled “Wrong environment loaded”](#wrong-environment-loaded) If `.env.production` is not loading, verify the environment flag is set in the container environment: ```bash docker run --rm -e APP_ENV=production -e PWD=/work -w /work ... ``` See the [Environments guide](/guides/environments/) for how `@currentEnv` selects files. # Encrypted deployments > Encrypt the env blob in your build output so secrets are never stored in plaintext When deploying SSR applications to **serverless platforms** (like Vercel, Netlify, or Cloudflare Workers), varlock injects the fully resolved env data into your server-side build output so it’s available at runtime without needing the CLI or filesystem access. This is necessary because serverless environments don’t give you control over how the application boots. By default, this blob is **plaintext JSON**. Since it only appears in server-side code, it’s generally safe: your secrets are not exposed to the client. However, encrypting the blob adds protection against secrets leaking via **sourcemaps** that may be uploaded to error tracking services. ## Enabling encryption [Section titled “Enabling encryption”](#enabling-encryption) Add the [`@encryptInjectedEnv`](/reference/root-decorators/#encryptinjectedenv) root decorator to your `.env.schema`: .env.schema ```env-spec # @encryptInjectedEnv # --- SECRET_KEY= # @sensitive ``` You can also enable it conditionally for specific environments: .env.schema ```env-spec # @encryptInjectedEnv=forEnv(prod) # --- SECRET_KEY= # @sensitive ``` ## How it works [Section titled “How it works”](#how-it-works) 1. At **build time**, varlock encrypts the serialized env graph with **AES-256-GCM** before injecting it 2. The encrypted blob is prefixed with `varlock:v1:`, so you can verify encryption by inspecting your build output 3. At **runtime**, `initVarlockEnv()` detects the encrypted prefix and decrypts using `_VARLOCK_ENV_KEY` from the runtime environment 4. The key is **never baked into the build**. It must always come from the runtime environment Decryption uses `node:crypto` where available; edge runtimes without it (e.g. Vercel Edge middleware) automatically fall back to asynchronous Web Crypto, with request handling gated until the env is ready. ## Key management [Section titled “Key management”](#key-management) The encryption key (`_VARLOCK_ENV_KEY`) is managed differently depending on your platform: ### Local development [Section titled “Local development”](#local-development) No setup needed. Varlock **auto-generates a temporary key** when running dev servers. The key exists only in memory for the duration of the dev session. ### Cloudflare Workers [Section titled “Cloudflare Workers”](#cloudflare-workers) No setup needed. `varlock-wrangler deploy` **auto-generates a key** and uploads it alongside your encrypted env as a Cloudflare secret binding. The key persists across deploys. ### Vercel, Netlify, and other platforms [Section titled “Vercel, Netlify, and other platforms”](#vercel-netlify-and-other-platforms) You must set `_VARLOCK_ENV_KEY` as an environment variable on your platform. The key must be available at both **build time** (for encryption) and **runtime** (for decryption). 1. **Generate and set the key** * Vercel ```bash # set one key for preview varlock generate-key --plain | vercel env add _VARLOCK_ENV_KEY preview --sensitive # set one key for production varlock generate-key --plain | vercel env add _VARLOCK_ENV_KEY production --sensitive ``` You can use the same key for both `preview` and `production`, but using separate keys is usually safer because it isolates environments and reduces blast radius if one key is exposed. * Netlify ```bash netlify env:set _VARLOCK_ENV_KEY $(varlock generate-key --plain) --secret ``` * Other platforms Generate a key and set it as a secret/env var on your platform: * npm ```bash npm exec -- varlock generate-key ``` * pnpm ```bash pnpm exec -- varlock generate-key ``` * bun ```bash bunx varlock generate-key ``` * vlt ```bash vlx -- varlock generate-key ``` * yarn ```bash yarn exec -- varlock generate-key ``` * npm ```bash npm exec -- varlock generate-key ``` * pnpm ```bash pnpm exec -- varlock generate-key ``` * bun ```bash bunx varlock generate-key ``` * vlt ```bash vlx -- varlock generate-key ``` * yarn ```bash yarn exec -- varlock generate-key ``` 2. **Deploy your app** The build will fail with a clear error if `@encryptInjectedEnv` is enabled but `_VARLOCK_ENV_KEY` is not set. ## Supported integrations [Section titled “Supported integrations”](#supported-integrations) | Integration | How it works | | --------------------------------------------------- | ---------------------------------------------------------------------- | | **[Next.js](/integrations/nextjs/)** | Encrypts the env blob injected into the webpack/turbopack build output | | **[Vite](/integrations/vite/)** | Encrypts the env blob when using `ssrInjectMode: 'resolved-env'` | | **[Astro](/integrations/astro/)** | Uses the Vite integration; encryption applies to SSR adapter builds | | **[SvelteKit](/integrations/sveltekit/)** | Uses the Vite integration; encryption applies to SSR builds | | **[TanStack Start](/integrations/tanstack-start/)** | Uses the Vite integration; encryption applies to SSR builds | | **[Cloudflare Workers](/integrations/cloudflare/)** | Encrypts the `__VARLOCK_ENV` secret binding uploaded at deploy time | | **[Expo](/integrations/expo/)** | Encrypts the env blob used by the Metro bundler for server routes | ## Verifying encryption [Section titled “Verifying encryption”](#verifying-encryption) After building with encryption enabled, you can inspect your build output to confirm the blob is encrypted: ```bash # Look for the encrypted prefix in build output grep -r 'varlock:v1:' dist/ .next/server/ 2>/dev/null ``` If encryption is working, you’ll see `varlock:v1:` followed by a base64-encoded ciphertext instead of raw JSON. ## Alternative: manual key without the decorator [Section titled “Alternative: manual key without the decorator”](#alternative-manual-key-without-the-decorator) You can also enable encryption without the `@encryptInjectedEnv` decorator by setting `_VARLOCK_ENV_KEY` in your environment. When present, varlock will encrypt the blob, but it won’t enforce the key’s presence or auto-generate it. # Environments > Best practices for managing multiple environments with varlock One of the main benefits of using environment variables is the ability to boot your application with configuration intended for different environments (e.g., development, preview, staging, production, test). You may use both [functions](/reference/functions/) and/or environment-specific `.env` files (e.g., `.env.production`) to alter configuration accordingly in a declarative way. Plus the additional guardrails provided by `varlock` also make this much safer no matter where values come from. environment-specific files are optional While many have traditionally shied away from using environment-specific `.env` files due to fear of committing sensitive values, the ability to set values using [plugins](/guides/plugins/) makes it easier to securely, and collaboratively, manage these values. ### Process overrides [Section titled “Process overrides”](#process-overrides) `varlock` will always treat environment variables passed into the process with the most precedence. Generally, we recommend moving as much configuration as possible into your `.env` files, but there are cases where you may want to override specific values at runtime, either from the environment itself, or by prepending them to your command (e.g., `APP_ENV=prod pnpm run build`). At the very least, you’ll often need to inject an environment flag (e.g., `APP_ENV`) and a *secret-zero* which allows access to the rest of your secrets in deployed environments. For local workflows, consider [device-local encryption](/guides/local-encryption/) to secure local secret-zero handling. That said, as a first step to adopting `varlock`, you could rely entirely on process overrides to inject all config values, but still benefit from having a clear schema with validation applied to them. ### Loading environment-specific `.env` files [Section titled “Loading environment-specific .env files”](#loading-environment-specific-env-files) Any environment-specific files (e.g., `.env.development`) will automatically be loaded if they match the value of the *current environment* as set by the [`@currentEnv`](/reference/root-decorators/#currentenv) root decorator in your `.env.schema` file. The files are applied with a specific precedence (increasing): * `.env.schema` - your schema file, which can also contain default values * `.env` - ⚠️ **not recommended**, will be loaded but better to use others * `.env.local` - local overrides *(gitignored)* * `.env.[currentEnv]` - environment-specific values * `.env.[currentEnv].local` - environment-specific local overrides *(gitignored)* Auto-detect with `VARLOCK_ENV` Instead of managing your own environment flag, you can use the built-in `$VARLOCK_ENV` variable which auto-detects the environment from your CI/deploy platform. See the [builtin variables reference](/reference/builtin-variables/) for details. .env.schema ```env-spec # @currentEnv=$VARLOCK_ENV ``` For example, consider the following `.env.schema`: .env.schema ```env-spec # @currentEnv=$APP_ENV # --- # @type=enum(development, test, staging, production) APP_ENV=development ``` Your environment flag key is set to `APP_ENV`, which has a default value of `development` - meaning that `.env.development` and `.env.development.local` will be loaded if they exist. To tell `varlock` to load `.env.staging` instead, you must set `APP_ENV` to `staging` - usually using an override passed into the process. For example: ```bash APP_ENV=staging varlock run -- node my-test-script.js ``` Loading `.env.local` in `test` environment Some tools ([dotenv-flow](https://github.com/kerimdzhanov/dotenv-flow), [Next.js](https://nextjs.org/docs/pages/guides/environment-variables#test-environment-variables), etc) make a special exception to skip loading `.env.local` if the current environment is `test`. Others tools ([Vite](https://vite.dev/config/#env-variables)) do not have any special handling. We chose to follow Vite’s lead, and instead provide a way to explicitly opt-in to that behavior: .env.local ```env-spec # @disable=forEnv(test) ``` Next.js precedence order Unlike Varlock (which matches Vite and dotenv-flow), [Next.js](https://nextjs.org/docs/pages/guides/environment-variables#environment-variable-load-order) swaps the order of precedence for `.env.local` vs `.env.[currentEnv]`. ## Advanced logic using functions [Section titled “Advanced logic using functions”](#advanced-logic-using-functions) On some platforms, you may not have full control over a build or boot command or the env vars passed into them. In this case, we can use functions to transform other env vars provided by the platform into the environment flag value we want. We can use [`remap()`](/reference/functions#remap) to transform a value according to a lookup, along with [regex literals](/reference/functions#regex-literals) if we need to match a pattern instead of an exact value. For example, on the Cloudflare Workers CI platform, we get the current branch name injected as `WORKERS_CI_BRANCH`, which we can use to determine which environment to load: .env.schema ```env-spec # @currentEnv=$APP_ENV # --- # set to current branch name when build is running on Cloudflare CI, empty otherwise WORKERS_CI_BRANCH= # @type=enum(development, preview, production, test) APP_ENV=remap($WORKERS_CI_BRANCH, "main", production, /.*/, preview, undefined, development) ``` You’ll notice that `test` is one of the possible enum values, but it is not listed in the remap. When running tests, you would just explicitly set `APP_ENV` when invoking your command. ```bash APP_ENV=test varlock run -- your-test-command # or if your command is loading varlock internally APP_ENV=test your-test-command ``` or you could run a production style build locally `APP_ENV=production varlock run -- your-build-command` Tip You can also use the [`forEnv()` helper](/reference/functions/#forenv) to dynamically set whether configuration items are required or optional based on the current environment. ## Setting a *default* environment flag [Section titled “Setting a default environment flag”](#setting-a-default-environment-flag) You can set the default environment flag directly when running CLI commands using the `--env` flag: ```bash varlock load --env production ``` This is only useful if you do not want to create a new env var for your env flag, and you are only using varlock via CLI commands. Mostly it is used internally by some integrations to match existing default behavior, and should not be used otherwise. Caution If `@currentEnv` is used, this will be ignored! ## Using `currentEnv` in Turborepo [Section titled “Using currentEnv in Turborepo”](#using-currentenv-in-turborepo) Turborepo users should be aware of a common pitfall when using `varlock`’s `@currentEnv` in monorepos managed by Turborepo, especially since Turborepo v2.0+ now enables **Strict Environment Mode** by default. ### The Problem [Section titled “The Problem”](#the-problem) Turborepo, when running tasks, filters the environment variables available to each task. By default in Strict Mode, **only** variables listed in the `env` or `globalEnv` keys in your `turbo.json` are passed to your scripts. This means that if your environment flag set by `@currentEnv` (e.g., `APP_ENV`) is not explicitly listed, it will not be available to your process, even if you set it in your shell or CI environment. This can cause `varlock` to load the wrong environment, or fail to load the correct `.env.[currentEnv]` file. ### Solution: Add your environment flag to turbo.json [Section titled “Solution: Add your environment flag to turbo.json”](#solution-add-your-environment-flag-to-turbojson) To ensure your environment flag variable is always available to your scripts, add it to the `env` or `globalEnv` section of your `turbo.json`: turbo.json ```json { "globalEnv": ["APP_ENV"], "tasks": { "build": { "env": ["APP_ENV"] }, "dev": { "env": ["APP_ENV"] } } } ``` * Use `globalEnv` if the variable should be available to all tasks. * Use `env` under a specific task if only needed for that task. Now when you run the following: ```bash APP_ENV=production turbo run build ``` it will load the correct `.env.production` file because the override for `APP_ENV` is passed correctly to `turbo` and in turn to `varlock`. Tip Substitute whatever your environment flag is for `APP_ENV` in the above example. We’re *only* passing `APP_ENV` because that is the variable you are most likely going to want to override in your scripts. If there are other variables you want to pass to your scripts, they will need to be explicitly added as well. ### Setting the Environment Flag [Section titled “Setting the Environment Flag”](#setting-the-environment-flag) When running locally, or on a platform you control, you can set the env flag explicitly as an environment variable. However on some cloud platforms, there is a lot of magic happening, and the ability to set environment variables per branch is limited. In these cases you can use functions to transform env vars injected by the platform, like a current branch name, into the value you need. #### Local/Custom Scripts [Section titled “Local/Custom Scripts”](#localcustom-scripts) You can set the env var explicitly when you run a command, but often you will set it in `package.json` scripts: package.json ```json "scripts": { "build:preview": "APP_ENV=preview next build", "start:preview": "APP_ENV=preview next start", "build:prod": "APP_ENV=production next build", "start:prod": "APP_ENV=production next start", "test": "APP_ENV=test jest" } ``` #### Vercel [Section titled “Vercel”](#vercel) You can use the injected `VERCEL_ENV` variable to match their concept of environment types, while adding your own additional options. .env.schema ```env-spec # @currentEnv=$APP_ENV # --- # @type=enum(development, preview, production) VERCEL_ENV= # @type=enum(development, preview, production, test) APP_ENV=fallback($VERCEL_ENV, development) ``` For more granular environments, use the branch name in `VERCEL_GIT_COMMIT_REF` (see Cloudflare example below). #### Cloudflare Workers Build [Section titled “Cloudflare Workers Build”](#cloudflare-workers-build) Use the branch name in `WORKERS_CI_BRANCH` to determine the environment: .env.schema ```env-spec # @currentEnv=$APP_ENV # --- WORKERS_CI_BRANCH= # @type=enum(development, preview, production, test) APP_ENV=remap($WORKERS_CI_BRANCH, "main", production, /.*/, preview, undefined, development) ``` # Imports > Learn how to use the @import decorator to share environment variables across files and services The [`@import()` root decorator](/reference/root-decorators/#import) allows you to import schema and/or values from other sources (currently just `.env` files), making it easy to share config across services within a monorepo, split up large schemas, or reuse pre-defined schemas. Multiple `@import()` calls may be used, and an imported source may itself import more sources. **Basic examples:** ```env-spec # @import(./.env.imported) # import specific file # @import(./env-dir/) # import directory # @import(./.env.partial, pick=[K1, K2]) # import specific keys # @import(~/.env.shared) # import from home directory ``` ## Import source types [Section titled “Import source types”](#import-source-types) The first argument to `@import()` specifies where to look for file(s) to import. Currently only local file imports are supported, but we plan to support importing over http in a style similar to Deno’s http imports. For now, all imported files must be `.env` files (and may contain @env-spec decorators), but in the future, we may also support other formats (e.g., JSON, YAML, etc.) or even JS/TS files. ### Single file [Section titled “Single file”](#single-file) * Path must begin with `./`, `../`, `/`, or `~/` * Imported file name must be begin with `.env.` ```env-spec # @import(./.env.common) ``` Home directory (`~`) You can import files from your home directory using the `~/` prefix. This is useful for sharing personal configuration across multiple projects without placing files inside any specific repo. ```env-spec # @import(~/.env.shared) ``` ### Directory [Section titled “Directory”](#directory) * Path must begin with `./`, `../`, `/`, or `~/` * Path must end with a trailing `/` * Multiple `.env.*` files will be detected and loaded, based on the current environment flag, similar to what happens in the current directory (see [environments guide](/guides/environments#loading-environment-specific-env-files)) * The environment flag value will be inherited, unless another `@currentEnv` is defined within the directory’s `.env.schema` ```env-spec # @import(../shared-config-dir/) ``` File vs directory: which should I import? * **Import a single file** when you want exactly that file’s contents: a shared `.env.common`, or specific keys from another schema via `pick`. * **Import a directory** (trailing `/`) when you want everything that directory would load on its own: its `.env.schema` **plus** sibling `.env`, `.env.local`, and environment-specific files, resolved by the current environment flag. This distinction matters when pulling in a root or sibling package. Importing its `.env.schema` file alone brings only the schema; importing the package’s **directory** also brings its values (including `.env.local` and `.env.[currentEnv]`). For the common “import the repo root” case in a monorepo, use the directory form (`# @import(../../)`). ## Partial imports [Section titled “Partial imports”](#partial-imports) By default, all items will be imported, but you can restrict which keys are brought in: * `pick=[...]` imports only an allowlist of keys * `omit=[...]` imports everything except a denylist of keys * You can’t use both in one import, and both accept simple globs (`*`, `?`) * If there is a chain of imports, an item is only imported if every ancestor import includes it (filters intersect) ```env-spec # @import(./.env.imported, pick=[KEY1, KEY2]) # allowlist # @import(./.env.imported, pick=[API_*]) # globs work too # @import(./.env.imported, omit=[SECRET]) # denylist ``` Positional keys are deprecated Older docs and configs list keys as positional args (`@import(./.env.imported, KEY1, KEY2)`). This still works but is deprecated and will warn. Use `pick=[KEY1, KEY2]` instead. ## Conditional imports [Section titled “Conditional imports”](#conditional-imports) While you can use the [`@disable`](/reference/root-decorators/#disable) root decorator to disable a file *from within that file*, you can also use the `enabled` parameter of the `@import()` decorator to conditionally load the file. The `enabled` parameter accepts any expression that evaluates to a boolean. It can be combined with partial imports to only import specific keys from a file. **Example:** .env.schema ```env-spec # Combine with partial imports # @import(./.env.features, pick=[FEATURE_X], enabled=eq($ENABLE_X, "true")) # --- ENABLE_X=true ``` ## Optional imports [Section titled “Optional imports”](#optional-imports) By default, `@import()` will cause a loading error if the specified file or directory does not exist. You can use the `allowMissing` parameter to make an import optional - if the file or directory doesn’t exist, it will be silently skipped without causing an error. The `allowMissing` parameter accepts a boolean value (defaults to `false`). It can be combined with partial imports and the `enabled` parameter. **Example:** .env.schema ```env-spec # Import if exists, skip if not # @import(./.env.local, allowMissing=true) ``` **Combine with other parameters:** .env.schema ```env-spec # Optional partial import with conditional loading # @import(./.env.features, pick=[FEATURE_X], enabled=true, allowMissing=true) ``` ## Import precedence and merging multiple sources [Section titled “Import precedence and merging multiple sources”](#import-precedence-and-merging-multiple-sources) Varlock is designed to load multiple definitions for a single item and merge them together. The common case would be taking schema info from `.env.schema` and overriding a value from another source (e.g., `.env.local`, `.env.production`, etc.), but there are many cases where root decorators, item decorators, and descriptions may be merged as well. To do this, we usually walk our data sources in decreasing order of precedence, until we find something defined for the value/decorator/etc we are evaluating. **Precedence rules are:** * Imported files are processed in order, with later imports overriding previous imports * Definitions and root decorators in the importing file override those in files it imports * For a directory, the precedence order is `.env.schema` < `.env` < `.env.local` < `.env.{currentEnv}` < `.env.{currentEnv}.local` For example, given a `.env.local` and a `.env.schema` that imports 2 files: .env.schema ```env-spec # @import(./.env.import1) # @import(./.env.import2) ``` The precedence order would be `.env.import1` < `.env.import2` < `.env.schema` < `.env.local`. Meaning if there was a value for `ITEM` in all 4 files, the final value used would be the one from `.env.local`. ## More details [Section titled “More details”](#more-details) * Root decorators that affect individual items (e.g., `@defaultRequired`) affect only the items that are defined in the file, not those in imported files * An item with no value at all (e.g., `ITEM=`) will be skipped when looking for a value / function to use, but its presence can be used to add other decorators/description to the item * If an imported file is marked with [`@disable`](/reference/root-decorators/#disable), it and any files it imports are skipped entirely * Importing the same source from multiple places (a “diamond”) is fine: it is loaded once and reused ## When things go wrong [Section titled “When things go wrong”](#when-things-go-wrong) ### `environment flag "..." must be defined within this schema` [Section titled “environment flag "..." must be defined within this schema”](#environment-flag--must-be-defined-within-this-schema) If you use [`@currentEnv`](/reference/root-decorators/#currentenv) to point at a variable (e.g. `# @currentEnv=$DEPLOY_ENV`) and that variable is only brought in via a **partial** `@import()`, varlock validates the env flag during schema initialization, before imported values are merged. The flag must be **defined in the same `.env.schema` file** that declares `@currentEnv`, not only in an imported file. This commonly appears in monorepos when a sub-package imports shared keys from a parent schema: .env.schema (sub-package) ```env-spec # @currentEnv=$DEPLOY_ENV # @import(../../../, pick=[DEPLOY_ENV, AWS_REGION]) # --- MY_SERVICE_URL=... ``` Running `varlock load` fails with: ```txt environment flag "DEPLOY_ENV" must be defined within this schema ``` **Fixes:** * Define the env flag locally in the file that uses `@currentEnv`, even if the value comes from elsewhere: .env.schema (sub-package) ```env-spec # @currentEnv=$DEPLOY_ENV # @import(../../../, pick=[DEPLOY_ENV, AWS_REGION]) # --- DEPLOY_ENV= MY_SERVICE_URL=... ``` * Move `@currentEnv` to the shared schema where the flag is already defined * Import the full directory (omit the key list) if the sub-package should inherit the parent’s `@currentEnv` handling ### Circular imports [Section titled “Circular imports”](#circular-imports) A circular chain of imports (e.g. `a` imports `b` which imports `a`) is not allowed. Varlock detects the cycle and fails with an explicit error showing the chain: ```txt Circular import detected: a/.env.schema -> b/.env.schema -> a/.env.schema ``` # Local encryption > Use varlock() to secure local untracked secrets with cross-platform encryption backends Varlock includes a built-in [`varlock()` function](/reference/functions/#varlock) that lets you secure local untracked secrets (typically in git-ignored env files like `.env.local`). This allows you to keep **everything out of plaintext** - even temporary local overrides, or a “secret-zero” which is needed by some plugins to load the rest of your sensitive data. Sensitive values will be stored *encrypted*, with the key linked to your local device, and requiring no extra configuration. The encryption mechanism varies per platform, but as it is tied to your device, **these values are not meant to be shared or committed to git**. .env.local ```diff(lang=env-spec) PLAINTEXT=shh-im-secret # 🚨 danger SECURED=varlock(local:abc123...) # ✅ secured at rest ``` Keychain also available on macOS For mac users, you can also use [keychain()](/plugins/macos-keychain/). The experience will be similar, but encrypted values are stored in the system keychain. ## Quick start - existing secrets [Section titled “Quick start - existing secrets”](#quick-start---existing-secrets) You likely already have some plaintext secrets in a `.env.local` file. If not you can create one, and add some. Ensure those items are marked as [`@sensitive`](/reference/item-decorators/#sensitive) in your schema. Then you can use [`varlock encrypt`](/reference/cli-commands/#encrypt) to encrypt them in-place: 1. Run `varlock encrypt --file .env.local` to encrypt them in-place 2. Sensitive plaintext values are replaced with `varlock("local:<***encrypted***>")` 3. Decryption happens automatically during `varlock load` / `varlock run` Only plaintext values of `@sensitive` items are encrypted, so you may run it multiple times. ## Using `varlock(prompt)` resolver [Section titled “Using varlock(prompt) resolver”](#using-varlockprompt-resolver) When you need to edit a value or add a new sensitive item, just set the value to `varlock(prompt)` and run `varlock load`. You will be prompted for the new value in a secure input prompt, and the encrypted value will be written back to the file automatically. .env.local ```env-spec EXISTING_ITEM=varlock(local:abc123...) NEW_ITEM=varlock(prompt) # will prompt you for new value ``` ## Using `varlock encrypt` CLI [Section titled “Using varlock encrypt CLI”](#using-varlock-encrypt-cli) You can also call the [`varlock encrypt`](/reference/cli-commands/#encrypt) CLI command to get a secure prompt to encrypt a single value. It will spit out an item you can copy/paste into your file: $ varlock encrypt ```text ◇ Enter the value you want to encrypt │ ▪▪▪▪▪▪▪▪ Copy this into your .env.local file and rename the key appropriately: SOME_SENSITIVE_KEY=varlock("local:ABC123...") ``` #### `--file` option [Section titled “--file option”](#--file-option) As outlined above, you can also run `varlock encrypt --file .env.local` to encrypt all sensitive plaintext values in a file in-place. This is a great way to quickly encrypt many secrets at once. ## Core commands [Section titled “Core commands”](#core-commands) Use [`varlock encrypt`](/reference/cli-commands/#encrypt) to create encrypted payloads: ```bash # Interactive: encrypt a single value varlock encrypt # Batch: encrypt all sensitive plaintext values in .env.local varlock encrypt --file .env.local ``` Use [`varlock reveal`](/reference/cli-commands/#reveal) to inspect decrypted values safely: ```bash varlock reveal # interactive - select and reveal varlock reveal API_KEY # securely reveal specific item varlock reveal API_KEY --copy # copy to clipboard ``` Use [`varlock lock`](/reference/cli-commands/#lock) to invalidate biometric session cache when stepping away: ```bash varlock lock ``` ## Backend selection overview [Section titled “Backend selection overview”](#backend-selection-overview) Varlock chooses the best available backend automatically: | Platform | Backend | Key Storage | Biometric | | ------------- | -------------------------- | -------------------------------- | ------------------------------------ | | macOS | Secure Enclave | Hardware Secure Enclave | Touch ID | | Windows | NCrypt TPM + Windows Hello | TPM (when available), else DPAPI | Windows Hello (face/fingerprint/PIN) | | Linux | TPM2 / Secret Service | TPM2 and/or system key store | Yes (when configured via polkit/PAM) | | All platforms | File-based fallback | `~/.varlock/` directory | No | If native capabilities are unavailable, varlock falls back to file-based local encryption. ## Unattended decryption (headless servers & CI) [Section titled “Unattended decryption (headless servers & CI)”](#unattended-decryption-headless-servers--ci) Local encryption isn’t just for laptops. On a headless host with a TPM (a home server, a CI runner, a container host), varlock seals the key to the machine’s TPM and decrypts automatically at load with **no prompt**. That makes it a good way to store a “secret-zero” (like a 1Password service account token) that bootstraps the rest of your secrets: .env.schema ```env-spec # @type=opServiceAccountToken @sensitive @internal OP_TOKEN=varlock("local:...") ``` On Linux this requires `tpm2-tools` and a TPM (see [Linux setup](#linux) below). The sealed value survives reboots and is bound to that specific machine. It can’t be copied to another host. This unattended path applies to backends whose key is unsealed to the host: Linux (TPM2) and Windows (NCrypt/TPM). **macOS is different:** Secure Enclave keys never leave the enclave, and every decrypt requires user presence (Touch ID or password), so there is no unattended decrypt on macOS. A headless Mac or CI runner falls back to file-based encryption instead. ### What this protects (and what it doesn’t) [Section titled “What this protects (and what it doesn’t)”](#what-this-protects-and-what-it-doesnt) Hardware-backed encryption protects your secrets **at rest**: a stolen disk, a leaked backup, or an env file committed by mistake can’t be decrypted off the machine. It does **not** protect against an attacker who is already running code as your user on that machine. They can ask the TPM to unseal the value exactly like varlock does. This is the same trade-off as tools like [`systemd-creds`](https://www.freedesktop.org/software/systemd/man/latest/systemd-creds.html), and it’s inherent to unattended decryption: if no human is present to approve, anything running as you can decrypt. If you want a presence gate (fingerprint / password / YubiKey via polkit + PAM) on every decrypt, register the policy: ```bash sudo varlock-local-encrypt setup --linux-biometrics ``` Once configured, varlock’s normal decrypt flow requires user presence, so everyday loads are no longer unattended. Enable this only on interactive machines, not headless servers. Treat this gate as a **consent prompt**, not an at-rest boundary. On Linux the TPM unseal isn’t bound to polkit, so (as noted above) other code already running as your user can still unseal directly. It means “a human approved this load,” not “only a human can ever decrypt.” ## Platform details & setup [Section titled “Platform details & setup”](#platform-details--setup) ### macOS [Section titled “macOS”](#macos) * Native Swift helper (Secure Enclave integration) * Uses system-native secure input / auth prompts * Includes a menu bar applet flow for native interactions * Hardware-backed key protection via Secure Enclave with biometric auth where supported ✅ No additional install/setup steps required ### Windows [Section titled “Windows”](#windows) * Native helper with **TPM-sealed** key protection when a TPM 2.0 chip is available (via NCrypt / Platform Crypto Provider) * Falls back to **DPAPI** (user-session-scoped) when TPM sealing is unavailable * **Windows Hello** gates interactive decrypts (fingerprint/face/PIN), separate from at-rest protection, same as before * Windows native and **WSL** workflows are both supported (WSL uses the Windows `.exe` via `--via-daemon`; Hello + TPM behavior is identical) * Automated daemon startup/installation behavior is built in for biometric session flows * WSL decrypt flows use a native bridge to the Windows daemon path ✅ No additional install/setup steps required #### Upgrading existing keys to TPM [Section titled “Upgrading existing keys to TPM”](#upgrading-existing-keys-to-tpm) New keys automatically use TPM sealing when available. Existing **DPAPI** keys are **auto-upgraded to TPM sealing on the next successful decrypt** (public key unchanged, no re-encryption of `.env` values). To upgrade without decrypting, use `varlock-local-encrypt rewrap-key --key-id varlock-default`. #### Windows Defender false positives [Section titled “Windows Defender false positives”](#windows-defender-false-positives) `varlock-local-encrypt.exe` is Varlock’s official local-encryption helper (bundled with the npm package and standalone CLI). Windows Defender may flag it as `Trojan:Win32/Wacatac.C!ml` or similar, a common machine-learning false positive on new, unsigned executables. The binary is safe when obtained from official Varlock releases. **Verify the download:** each [GitHub release](https://github.com/dmno-dev/varlock/releases) includes `SHA256SUMS.txt` with hashes for all native helpers. After installing, compare your file: ```powershell # PowerShell: run from the directory containing the .exe Get-FileHash varlock-local-encrypt.exe -Algorithm SHA256 ``` Match the hash against the `native-bins/win32-x64/varlock-local-encrypt.exe` line in `SHA256SUMS.txt` for your release version. **If quarantined:** restore the file from Windows Security → Protection history, or add an exclusion for your project directory. Reinstall from npm or the official release if needed. **Permanent fix:** official builds are moving to Authenticode code signing, which greatly reduces these alerts. Until your installed version is signed, verification via `SHA256SUMS.txt` is the recommended way to confirm authenticity. ### Linux [Section titled “Linux”](#linux) * Native Linux helper when available * User-presence verification via polkit/PAM (can support fingerprint/face/password depending system setup) Common packages/tools: * `tpm2-tools` (plus distro TPM2 libs such as `tpm2-tss`) * `polkit` for user-presence authorization flows * `xclip` or `xsel` for `varlock reveal --copy` Example installs: ```bash # Debian/Ubuntu sudo apt-get update sudo apt-get install -y tpm2-tools tpm2-tss policykit-1 xclip ``` ```bash # Fedora/RHEL variants sudo dnf install -y tpm2-tools tpm2-tss polkit xclip ``` If biometric/user-presence prompts are unavailable on Linux, complete policy setup (native helper command): ```bash sudo varlock-local-encrypt setup --linux-biometrics ``` # MCP Security > Using varlock to secure MCP clients and servers - protecting secrets in AI agent connections The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) enables AI agents to connect to external data sources and tools. When using MCP, you often need to handle sensitive configuration like API keys, database credentials, and authentication tokens. `varlock` provides a secure way to manage these secrets without exposing them in your configuration files or to AI agents. This guide covers three scenarios: * **Local MCP servers** using stdio transport with `varlock run` * **Remote MCP servers** using varlock’s Node.js integration * **Third-party MCP servers** using varlock to load secrets and pass them to the server ## Local MCP Servers with stdio [Section titled “Local MCP Servers with stdio”](#local-mcp-servers-with-stdio) For local development and testing, MCP servers often use stdio transport for communication with clients. This is perfect for using `varlock run` to securely load environment variables before starting your server. ### Server Setup [Section titled “Server Setup”](#server-setup) Create a `.env.schema` file for your MCP server: .env.schema ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(allowAppAuth=true) # @defaultSensitive=true # @defaultRequired=true # --- # Database connection for MCP server # @type=url DATABASE_URL= # API key for external service # @type=string(startsWith="sk_") EXTERNAL_API_KEY= # Authentication secret # @type=string(minLength=32) AUTH_SECRET= # Server configuration # @sensitive=false # @type=number(min=1024, max=65535) SERVER_PORT=3000 # @sensitive=false # @type=enum(debug, info, warn, error) LOG_LEVEL=info ``` Create your local `.env.local` file with values from your 1Password vault: .env.local ```env-spec DATABASE_URL=op(op://devTest/myVault/database-url) EXTERNAL_API_KEY=op(op://devTest/myVault/external-api-key) AUTH_SECRET=op(op://devTest/myVault/auth-secret) LOG_LEVEL=debug ``` Note We’re using 1Password as an example here, but you can use any secret management tool you prefer as long as it has a CLI to load values. Update your MCP server’s `package.json` to use `varlock run`: package.json ```json { "name": "my-mcp-server", "scripts": { "start": "varlock run -- node server.js", "dev": "varlock run -- node --watch server.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^0.4.0" } } ``` ### Docker (local) [Section titled “Docker (local)”](#docker-local) For containerized local development, create a Dockerfile that uses varlock: Dockerfile ```dockerfile FROM node:22-alpine # Install varlock RUN npm install -g @varlock/cli WORKDIR /app # Copy package files COPY package*.json ./ COPY pnpm-lock.yaml ./ # Install dependencies RUN npm install -g pnpm && pnpm install # Copy application files COPY . . # Build the application RUN pnpm build # Use varlock run to start the server CMD ["varlock", "run", "--", "node", "dist/server.js"] ``` Build and run your Docker container: ```bash # Build the image docker build -t my-mcp-server:latest . # Run the container (for testing) docker run --rm -it my-mcp-server:latest ``` ### Client Configuration [Section titled “Client Configuration”](#client-configuration) * Cursor Create a Cursor configuration file to connect to your local MCP server: \~/.cursor/mcp-servers.json ```json { "mcpServers": { "my-local-server": { "command": "npm", "args": ["start"], "cwd": "/path/to/your/mcp-server", "env": { "NODE_ENV": "development" } } } } ``` For local MCP servers running in Docker: In this case an off-the-shelf MCP server is used, so we need to use `varlock run` to load the `GITHUB_TOKEN` environment variable and pass it to the server. \~/.cursor/mcp-servers.json ```json { "mcpServers": { "github": { "command": "varlock", "args": [ "run", "--", "docker", "run", "--rm", "-i", "ghcr.io/github/github-mcp-server:latest" ], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } } } } ``` And the corresponding `.env.schema` file would look something like this: .env.schema ```env-spec # @defaultSensitive=true # @defaultRequired=true # --- # GitHub token # @type=string(startsWith="ghp_") GITHUB_TOKEN=op(op://devTest/myVault/github-token) ``` * Claude Desktop For Claude Desktop, create a configuration file: \~/.config/claude/desktop\_config.json ```json { "mcpServers": { "my-local-server": { "command": "npm", "args": ["start"], "cwd": "/path/to/your/mcp-server" } } } ``` For local MCP servers running in Docker: In this case an off-the-shelf MCP server is used, so we need to use `varlock run` to load the `GITHUB_TOKEN` environment variable and pass it to the server. \~/.config/claude/desktop\_config.json ```json { "mcpServers": { "github": { "command": "varlock", "args": [ "run", "--", "docker", "run", "--rm", "-i", "ghcr.io/github/github-mcp-server:latest" ], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } } } } ``` * Custom Client Here’s an example of a custom MCP client that uses varlock for its own configuration: client.ts ```typescript import 'varlock/auto-load'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { spawn } from 'child_process'; import { ENV } from 'varlock/env'; const client = new Client( { name: 'my-mcp-client', version: '1.0.0' }, { capabilities: { tools: {} } } ); // Start the server process with varlock const serverProcess = spawn('pnpm', ['start'], { cwd: ENV.MCP_SERVER_PATH, stdio: ['pipe', 'pipe', 'pipe'] }); const transport = new StdioClientTransport(serverProcess.stdin, serverProcess.stdout); await client.connect(transport); // Use the client to interact with your MCP server const result = await client.callTool({ name: 'my-tool', arguments: {} }); ``` For third-party MCP servers that require API keys: third-party-client.ts ```typescript import 'varlock/auto-load'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { spawn } from 'child_process'; import { ENV } from 'varlock/env'; async function connectToOpenAIServer() { const client = new Client( { name: 'openai-mcp-client', version: '1.0.0' }, { capabilities: { tools: {} } } ); const serverProcess = spawn('npx', [ '@modelcontextprotocol/server-openai', '--api-key', ENV.OPENAI_API_KEY ], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, OPENAI_API_KEY: ENV.OPENAI_API_KEY } }); const transport = new StdioClientTransport(serverProcess.stdin, serverProcess.stdout); await client.connect(transport); return client; } async function connectToGitHubServer() { const client = new Client( { name: 'github-mcp-client', version: '1.0.0' }, { capabilities: { tools: {} } } ); const serverProcess = spawn('npx', [ '@modelcontextprotocol/server-github', '--token', ENV.GITHUB_TOKEN ], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, GITHUB_TOKEN: ENV.GITHUB_TOKEN } }); const transport = new StdioClientTransport(serverProcess.stdin, serverProcess.stdout); await client.connect(transport); return client; } ``` ## Remote MCP Servers [Section titled “Remote MCP Servers”](#remote-mcp-servers) For production deployments, you’ll want to run MCP servers as standalone processes with varlock integrated directly into the server code. Note Code is for example purposes only. Server implementations will vary depending on the MCP server you’re using. See the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) for more information. ### Server Implementation [Section titled “Server Implementation”](#server-implementation) server.ts ```typescript import 'varlock/auto-load'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ENV } from 'varlock/env'; async function main() { const server = new Server( { name: 'my-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); // Register tools with access to secure configuration server.setRequestHandler('tools/call', async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'query-database': // Use secure database connection from config return await queryDatabase(ENV.DATABASE_URL, args); case 'call-external-api': // Use secure API key from config return await callExternalAPI(ENV.EXTERNAL_API_KEY, args); default: throw new Error(`Unknown tool: ${name}`); } }); const transport = new StdioServerTransport(process.stdin, process.stdout); await server.connect(transport); } async function queryDatabase(databaseUrl: string, args: any) { // Implementation using secure database URL console.log('Querying database with secure connection'); return { result: 'database query result' }; } async function callExternalAPI(apiKey: string, args: any) { // Implementation using secure API key console.log('Calling external API with secure key'); return { result: 'api call result' }; } main().catch(console.error); ``` ### Production Deployment [Section titled “Production Deployment”](#production-deployment) For production, create environment-specific schema files. See the [environments guide](/guides/environments) for detailed information on managing multiple environments with varlock. .env.schema ```env-spec # @defaultSensitive=true # @defaultRequired=true # @currentEnv=$APP_ENV # --- # env flag is used to determine which environment to load # default is development # @type=enum(development, staging, test, production) APP_ENV=development # Database connection # @type=url DATABASE_URL= # External API credentials # @type=string(startsWith="sk_") EXTERNAL_API_KEY= # Authentication # @type=string(minLength=32) AUTH_SECRET= # Server settings # @sensitive=false # @type=number(min=1024, max=65535) SERVER_PORT=3000 # @sensitive=false # @type=enum(debug, info, warn, error) LOG_LEVEL=info ``` .env.production ```env-spec DATABASE_URL=op(op://prodTest/prodVault/prod-database-url) EXTERNAL_API_KEY=op(op://prodTest/prodVault/prod-external-api-key) AUTH_SECRET=op(op://prodTest/prodVault/prod-auth-secret) SERVER_PORT=3000 LOG_LEVEL=warn ``` Then in the command to start the server, you can use the `varlock run` command to load the environment variables with the correct `currentEnv` environment override. ```bash APP_ENV=production varlock run -- node server.js ``` ## Security Best Practices [Section titled “Security Best Practices”](#security-best-practices) ### 1. Never Store Secrets in Plain Text [Section titled “1. Never Store Secrets in Plain Text”](#1-never-store-secrets-in-plain-text) Always use external secret management such as 1Password or the built-in env var management in your deployment platform. ```env-spec # ❌ Never do this API_KEY=sk_live_1234567890abcdef # ✅ Use external secret management API_KEY=op(op://devTest/myVault/api-key) ``` ### 2. Use Environment-Specific Schemas [Section titled “2. Use Environment-Specific Schemas”](#2-use-environment-specific-schemas) Create separate schema files for different environments. See the [environments guide](/guides/environments) for detailed information on managing multiple environments with varlock. .env.schema ```env-spec # @defaultSensitive=true # @currentEnv=$APP_ENV # --- # env flag is used to determine which environment to load # default is development # @type=enum(development, staging, test, production) APP_ENV=development # Common configuration DATABASE_URL= API_KEY= ``` .env.development ```env-spec DATABASE_URL=postgresql://localhost:5432/dev_db API_KEY=op(op://devTest/myVault/dev-api-key) ``` .env.production ```env-spec DATABASE_URL=op(op://prodTest/prodVault/prod-database-url) API_KEY=op(op://prodTest/prodVault/prod-api-key) ``` ### 3. Validate Sensitive Data [Section titled “3. Validate Sensitive Data”](#3-validate-sensitive-data) Use varlock’s validation features to ensure data integrity: .env.schema ```env-spec # @type=string(startsWith="sk_", minLength=20) API_KEY= # @type=url DATABASE_URL= ``` ### 4. Monitor and Log Securely [Section titled “4. Monitor and Log Securely”](#4-monitor-and-log-securely) Use varlock’s redaction features to prevent sensitive data from appearing in logs: ```typescript import 'varlock/auto-load'; import { ENV } from 'varlock/env'; // Sensitive values are automatically redacted in logs console.log('API Key:', ENV.API_KEY); // Shows: [xx▒▒▒▒▒] console.log('Database URL:', ENV.DATABASE_URL); // Shows: [xx▒▒▒▒▒] ``` Note Redaction is on by default, see [root decorators - redactLogs](/reference/root-decorators/#redactlogs) for more information. ## Docs MCP [Section titled “Docs MCP”](#docs-mcp) We also have a MCP server that allows you to search the Varlock docs. Yes, this getting a bit meta. The MCP server is available at: * (Streamable HTTP) * (Server-Sent Events) See below for tool-specific setup instructions. * Cursor Click below to install the MCP server in Cursor: [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=varlock-docs-mcp\&config=eyJjb21tYW5kIjoibnB4IG1jcC1yZW1vdGUgaHR0cHM6Ly9kb2NzLm1jcC52YXJsb2NrLmRldi9tY3AifQ%3D%3D) Or add the following to your `.cursor/mcp-servers.json` file: \~/.cursor/mcp.json ```json { "mcpServers": { "varlock-docs-mcp": { "command": "npx", "args": ["mcp-remote", "https://docs.mcp.varlock.dev/mcp"] } } } ``` * Claude To add a server in Claude Code, run the following command. Use `--scope user` so the server is available in **all** Claude Code sessions; without it, the install applies to the current project only. ```bash claude mcp add --scope user --transport http varlock-docs-mcp https://docs.mcp.varlock.dev/mcp ``` See [Claude’s documentation](https://docs.claude.com/en/docs/claude-code/mcp#option-1%3A-add-a-remote-http-server) for more information. * Codex To add the Varlock docs MCP server in Codex, run: ```bash codex mcp add varlock-docs-mcp --url https://docs.mcp.varlock.dev/mcp ``` * opencode To add a remote MCP server in Opencode, add the following to your `opencode.json` file: opencode.json ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "varlock-docs-mcp": { "type": "remote", "url": "https://docs.mcp.varlock.dev/mcp", "enabled": true, } } } ``` See [Opencode’s documentation](https://opencode.ai/docs/mcp-servers/#remote) for more information. * VS Code To add a remote MCP server in VS Code, add the following to your `.vscode/mcp.json` file: .vscode/mcp.json ```json { "servers": { "varlock-docs-mcp": { "type": "http", "url": "https://docs.mcp.varlock.dev/mcp" } } } ``` See [VS Code’s documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) for more information. ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn more about [varlock’s environment specification](/env-spec/overview) * Explore [available data types](/reference/data-types) for validation * Check out [function reference](/reference/functions) for external integrations * Read about [secrets management](/guides/secrets) best practices # Migrate from dotenv > How to migrate from dotenv (CLI and npm package) to varlock ## Why migrate from dotenv? [Section titled “Why migrate from dotenv?”](#why-migrate-from-dotenv) * **Validation**: Catch misconfigurations early with schema-driven validation. * **Security**: Redact secrets and prevent accidental leaks. * **Type-safety**: Generate types automatically for your config. * **External secrets**: Load secrets from providers like 1Password, AWS, and more. *** ## Migrating from dotenvx CLI [Section titled “Migrating from dotenvx CLI”](#migrating-from-dotenvx-cli) If you use `dotenvx` via the CLI, you can switch to `varlock run`: ```bash # Before (dotenv CLI) dotenvx run -- node app.js # env specific dotenvx run -f .env.staging -- node app.js # install varlock brew install dmno-dev/tap/varlock # After (varlock CLI) varlock run -- node app.js # To specify an environment, set your env flag (see your .env.schema) APP_ENV=staging varlock run -- node app.js ``` Note You can use multiple `.env` files (see the [Environments guide](/guides/environments/)). *** ## Migrating from dotenv npm package [Section titled “Migrating from dotenv npm package”](#migrating-from-dotenv-npm-package) Initialize your project with `varlock init` to install `varlock` and generate a `.env.schema` from any existing `.env` files. * npm ```bash npx varlock init ``` * pnpm ```bash pnpm dlx varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx varlock init ``` * yarn ```bash yarn dlx varlock init ``` Then to use `varlock` in your code, you can replace `dotenv/config` with `varlock/auto-load`: index.js ```diff // Before (dotenv) import 'dotenv/config'; import 'varlock/auto-load'; ``` Finally, you can remove `dotenv` from your dependencies: * npm ```bash npm uninstall dotenv ``` * pnpm ```bash pnpm remove dotenv ``` * bun ```bash bun remove dotenv ``` * yarn ```bash yarn remove dotenv ``` * vlt ```bash vlt uninstall dotenv ``` ## Using overrides [Section titled “Using overrides”](#using-overrides) If `dotenv` is being used under the hood of one of your dependencies, you can use `overrides` to swap in `varlock` instead. * npm See [NPM overrides docs](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides) package.json ```diff { +"overrides": { +"other-dep": { +"dotenv": "npm:varlock" + } + } } ``` * yarn See [yarn resolutions docs](https://yarnpkg.com/configuration/manifest#resolutions) package.json ```diff { +"resolutions": { +"**/dotenv": "npm:varlock" + }, } ``` **In a monorepo, this override must be done in the monorepo’s root package.json file!** * pnpm * pnpm version 10+ See [pnpm v10 overrides docs](https://pnpm.io/settings#overrides) pnpm-workspace.yaml ```diff +overrides: +"dotenv": "npm:varlock" ``` **This must be set in `pnpm-workspace.yaml`, which lives at the root of your repo, regardless of whether you are using a monorepo or not.** * pnpm version 9 ### pnpm version 9 [Section titled “pnpm version 9”](#pnpm-version-9) See [pnpm v9 overrides docs](https://pnpm.io/9.x/package_json#pnpmoverrides) package.json ```diff { +"pnpm": { +"overrides": { +"dotenv": "npm:varlock" + } + } } ``` **In a monorepo, this override must be done in the monorepo’s root package.json file!** * pnpm version 10+ See [pnpm v10 overrides docs](https://pnpm.io/settings#overrides) pnpm-workspace.yaml ```diff +overrides: +"dotenv": "npm:varlock" ``` **This must be set in `pnpm-workspace.yaml`, which lives at the root of your repo, regardless of whether you are using a monorepo or not.** * pnpm version 9 ### pnpm version 9 [Section titled “pnpm version 9”](#pnpm-version-9) See [pnpm v9 overrides docs](https://pnpm.io/9.x/package_json#pnpmoverrides) package.json ```diff { +"pnpm": { +"overrides": { +"dotenv": "npm:varlock" + } + } } ``` **In a monorepo, this override must be done in the monorepo’s root package.json file!** *** ## Further reading [Section titled “Further reading”](#further-reading) * [Environments guide](/guides/environments) * [Schema guide](/guides/schema) * [Reference: CLI commands](/reference/cli-commands) * [Reference: Item decorators](/reference/item-decorators) # Monorepos > Patterns for using varlock in monorepos and multi-service repositories Monorepos often mix shared platform config with app-specific settings. Varlock supports both per-package schemas and shared root config, with [`@import()`](/reference/root-decorators/#import) to compose them. This guide walks through common layout choices, incremental adoption, CI, and containers. ## Schema layout [Section titled “Schema layout”](#schema-layout) The recommended pattern is **one `.env.schema` per project**. Each app or service owns a schema next to its code and imports whatever shared config it needs from the repo root or sibling packages. Avoid funneling every variable into a single central env directory; keep each project’s config with the project. ### One schema per project (recommended) [Section titled “One schema per project (recommended)”](#one-schema-per-project-recommended) Give each app or service its own `.env.schema`. This keeps config next to the code that uses it, lets you adopt varlock one package at a time, and works well even when services are mostly independent. ```text .env.schema # shared root config (imported by apps that need it) packages/ web/.env.schema api/.env.schema ``` Varlock resolves env files from each package’s **current working directory**. It does not walk up to a parent directory automatically. Use [`@import()`](/guides/import/) when a package needs config defined elsewhere. Packages with no env vars A library or utility package that doesn’t read any environment variables doesn’t need a `.env.schema` at all. Only add one where a package actually loads config. ### Sharing config from the root or siblings [Section titled “Sharing config from the root or siblings”](#sharing-config-from-the-root-or-siblings) Put cross-cutting config (database URLs used by many services, shared feature flags, platform identifiers) in a schema at the repo root, then **import the root directory** from each project that needs it: packages/web/.env.schema ```env-spec # Import only the shared keys this app needs # @import(../../, pick=[DATABASE_URL, REDIS_URL, SENTRY_*]) # @import(../api/, pick=[API_PUBLIC_URL]) # --- # App-specific config # @type=url @required PUBLIC_SITE_URL= ``` By default every key in the imported source is brought in, but importing only the keys an app actually needs is usually the cleaner default. Use `pick=[...]` (allowlist) or `omit=[...]` (denylist) so each schema declares exactly what it depends on. Importing the **directory** (`../../`) rather than the schema file (`../../.env.schema`) means all files like `.env.local` and [environment-specific](/guides/environments/) are loaded too, not just the schema, following the same precedence as the current directory. Imported definitions merge with the importing schema, and the importing file wins. See the [Imports guide](/guides/import/) for partial imports, precedence, and conditional loading. Use `@import()` to pull in a root or shared schema without copying keys, to split a large schema into focused files (e.g. `.env.database`, `.env.features`), or to share a directory of common env files across services (`# @import(../../shared-config/)`). You can also **import directly from a sibling package**, which lets you keep a config item with its logical owner instead of hoisting everything to the root. Define the item once in the package that owns it, then `pick` just what you need, like the `../api/` import in the example above. Avoid circular imports Imports must flow in one direction. If `web` imports `api` and `api` also imports `web` (directly or through a chain), varlock detects the cycle and loading fails with an explicit error naming the chain (e.g. `Circular import detected: web/.env.schema -> api/.env.schema -> web/.env.schema`). Keep each shared item with a single owner and import one way; if two packages genuinely need each other’s values, hoist those keys into a common file (e.g. the repo root) that both import. Shared environment flag If several projects share an environment flag like `APP_ENV`, define it in the shared schema and import it. For per-service environment variations, see [Loading environment-specific `.env` files](/guides/environments#loading-environment-specific-env-files). ### Pointing varlock at the right directory [Section titled “Pointing varlock at the right directory”](#pointing-varlock-at-the-right-directory) In most cases you don’t need anything here. A `.env.schema` in each package, loaded from its working directory, is the right default. Reach for the options below only when a package’s env files live somewhere other than its cwd. When an app’s env files live outside its default cwd (or you need multiple roots), set `varlock.loadPath` in that package’s `package.json`: apps/web/package.json ```json { "varlock": { "loadPath": "./" } } ``` You can also pass an explicit path on the CLI with `--path` (or `-p`), useful in CI or scripts that run from the monorepo root: * npm ```bash npm exec -- varlock load --path apps/web/ ``` * pnpm ```bash pnpm exec -- varlock load --path apps/web/ ``` * bun ```bash bunx varlock load --path apps/web/ ``` * vlt ```bash vlx -- varlock load --path apps/web/ ``` * yarn ```bash yarn exec -- varlock load --path apps/web/ ``` For loading from multiple directories in one package, see [Loading from multiple directories](/integrations/vite/#loading-from-multiple-directories) in the Vite integration docs (the same `loadPath` array works for all commands). ## Next.js apps [Section titled “Next.js apps”](#nextjs-apps) Next.js requires a workspace-root `@next/env` override and has extra footguns when only some apps use varlock. See [Next.js in a monorepo](/integrations/nextjs/#monorepos) for package-manager overrides, incremental adoption, and troubleshooting, then return here for shared schema layout and CI patterns. ## Turborepo and strict env mode [Section titled “Turborepo and strict env mode”](#turborepo-and-strict-env-mode) Turborepo v2+ enables **Strict Environment Mode** by default. Tasks only receive env vars that are listed in `turbo.json`, so any var varlock reads from the ambient environment (not just your `@currentEnv` flag) must be declared there, or it won’t reach the task. This includes: * your environment flag (e.g. `APP_ENV`), or varlock may load the wrong `.env.[currentEnv]` file * the **CI-detection vars** that built-ins like [`VARLOCK_ENV`](/reference/builtin-variables/#varlock_env) rely on (`CI`, `VERCEL`, `WORKERS_CI_BRANCH`, etc.) * any var passed in at the root `turbo` invocation, or otherwise present in the environment, that a `VARLOCK_*` setting or a resolver in your schema depends on Declare these in `globalEnv` (or per-task `env`) in `turbo.json`. Full explanation and examples are in [Using `currentEnv` in Turborepo](/guides/environments#using-currentenv-in-turborepo). ## Incremental adoption [Section titled “Incremental adoption”](#incremental-adoption) You do not need to migrate every package at once. A practical order: 1. **Pick one app**: run `varlock init` in that package and commit its `.env.schema`. 2. **Wire the integration** for that stack only (Next.js plugin, Vite plugin, or `varlock run` for scripts). 3. **Expand shared config**: extract common keys to a root or shared schema and `@import()` them from apps as you go. 4. **Leave untouched apps alone**: packages without a `.env.schema` should keep using their existing env loading until you migrate them. CLI-only services Services that do not need a framework integration can start with [`varlock run`](/reference/cli-commands/#run) and a local `.env.schema` without changing root dependency overrides. ## Typed `ENV` across packages [Section titled “Typed ENV across packages”](#typed-env-across-packages) By default, [`@generateTsTypes`](/reference/root-decorators/#generatetstypes) globally augments the `varlock/env` module so `import { ENV } from 'varlock/env'` is typed. There is only one `varlock/env` module in a TypeScript program, so when several packages each generate types with **different** schemas, their augmentations merge, so a package can end up seeing keys it never declared, or hit conflicting types. To keep each package’s `ENV` isolated, set `exposeEnv=local`. It emits a package-local file that re-exports the runtime `ENV` typed to *that* package’s schema, with no global augmentation. The `process.env` / `import.meta.env` augmentations default off too, so nothing from this package merges into the global type scope: packages/api/.env.schema ```env-spec # @generateTsTypes(path=./env.ts, exposeEnv=local) ``` Then import from the generated file instead of `varlock/env`: packages/api/src/server.ts ```ts import { ENV } from '../env'; // typed to this package's schema only ``` Runtime behavior is identical (it’s the same underlying proxy); only the types are scoped locally. The output path must be a `.ts` file (not `.d.ts`) since it contains a runtime re-export. ## Mixed stacks (JavaScript and other languages) [Section titled “Mixed stacks (JavaScript and other languages)”](#mixed-stacks-javascript-and-other-languages) Monorepos often combine Node apps with Python, Go, or other runtimes. Varlock’s CLI works the same everywhere: load and validate from a schema, then exec your command with the resolved env injected. * npm ```bash npm exec -- varlock run -- python manage.py runserver ``` * pnpm ```bash pnpm exec -- varlock run -- python manage.py runserver ``` * bun ```bash bunx varlock run -- python manage.py runserver ``` * vlt ```bash vlx -- varlock run -- python manage.py runserver ``` * yarn ```bash yarn exec -- varlock run -- python manage.py runserver ``` Run the command from the service directory (or pass `--path` to its env folder). Each language can keep its own `.env.schema`; there is no requirement to share a single root file. For more detail, see [Other languages](/integrations/other-languages/). ## Container builds [Section titled “Container builds”](#container-builds) Container images often copy a single app directory, not the whole monorepo. If your schema uses `@import()` to pull files from sibling packages or the repo root, those paths must exist in the build context, otherwise the load graph fails at build time. The [Docker guide](/guides/docker/) covers the official image, multi-stage copies of the varlock binary, and running `varlock run` as your container entrypoint. Today you may need to copy imported env files explicitly in your Dockerfile (mirroring the import tree). A planned **`varlock flatten`** command will collapse an import graph into local files for slim Docker contexts. See the [Docker guide](/guides/docker/#monorepos-and-partial-build-context). # OIDC Workload Identity > Authenticate with secret providers using OIDC tokens from your deployment platform, with no long-lived credentials Many deployment platforms issue short-lived [OIDC](https://openid.net/developers/how-connect-works/) tokens that your application can exchange for temporary credentials with secret providers. This eliminates the need to store long-lived API keys or service account credentials in your deployment environment: no “secret zero” needed to fetch the rest of your secrets. Varlock supports OIDC workload identity federation across multiple plugins and platforms. In most cases, the OIDC token is **auto-detected** from your deployment platform, so you just need to configure the provider side. ## How it works [Section titled “How it works”](#how-it-works) ```plaintext 1. Your app starts on a deployment platform (Vercel, GitHub Actions, etc.) 2. Varlock auto-detects the platform and requests an OIDC token 3. The token is exchanged with your secret provider for temporary credentials 4. Secrets are fetched using the temporary credentials (valid 15 min – 1 hour) ``` No long-lived secrets are stored anywhere in the deployment environment. ## Supported plugins [Section titled “Supported plugins”](#supported-plugins) | Plugin | OIDC mechanism | Setup guide | | -------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------- | | [AWS Secrets](/plugins/aws-secrets/) | STS `AssumeRoleWithWebIdentity` | [OIDC setup](/plugins/aws-secrets/#oidc-authentication) | | [Azure Key Vault](/plugins/azure-key-vault/) | Federated credential (JWT assertion) | [OIDC setup](/plugins/azure-key-vault/#oidc-federated-credentials) | | [Google Secret Manager](/plugins/google-secret-manager/) | Workload Identity Federation | [OIDC setup](/plugins/google-secret-manager/#oidc-workload-identity-federation) | | [HashiCorp Vault](/plugins/hashicorp-vault/) | JWT/OIDC auth method | [OIDC setup](/plugins/hashicorp-vault/#jwtoidc-authentication) | | [Infisical](/plugins/infisical/) | OIDC machine identity | [OIDC setup](/plugins/infisical/#oidc-authentication) | | [Akeyless](/plugins/akeyless/) | OIDC access type | [OIDC setup](/plugins/akeyless/#oidc-authentication) | ## Supported platforms [Section titled “Supported platforms”](#supported-platforms) Varlock auto-detects OIDC tokens from these deployment platforms: | Platform | How it works | Token source | Setup | | ------------------ | --------------------------------------------------- | --------------------------- | -------------------------- | | **Vercel** | Token available in builds and serverless functions | `VERCEL_OIDC_TOKEN` env var | [details](#vercel) | | **GitHub Actions** | Requires `permissions: id-token: write` in workflow | Token request API | [details](#github-actions) | | **GitLab CI** | Requires `id_tokens` in `.gitlab-ci.yml` | `CI_JOB_JWT_V2` env var | [details](#gitlab-ci) | | **Fly.io** | Available to all Fly Machines | Internal API endpoint | [details](#flyio) | | **GCP Cloud Run** | Available via metadata server | Metadata endpoint | [details](#gcp-cloud-run) | For platforms not listed above, you can pass an explicit OIDC token using the `oidcToken` parameter on each plugin. See [Custom / other platforms](#custom--other-platforms). ### Vercel [Section titled “Vercel”](#vercel) OIDC tokens are available automatically in Vercel builds and serverless functions. No additional configuration is needed on the Vercel side. To use with a secret provider, you need to configure the provider to trust Vercel’s OIDC issuer: * **Issuer URL:** `https://oidc.vercel.com` * **Audience:** Varies by provider (see plugin-specific docs) See [Vercel OIDC documentation](https://vercel.com/docs/oidc) for details. ### GitHub Actions [Section titled “GitHub Actions”](#github-actions) Add `id-token: write` permission to your workflow: ```yaml jobs: deploy: permissions: id-token: write contents: read steps: - uses: actions/checkout@v7 # ... your build/deploy steps ``` * **Issuer URL:** `https://token.actions.githubusercontent.com` * **Subject claim:** `repo:/:ref:refs/heads/` (varies by trigger) See [GitHub Actions OIDC documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) for details. ### GitLab CI [Section titled “GitLab CI”](#gitlab-ci) Add `id_tokens` to your `.gitlab-ci.yml`: ```yaml job_name: id_tokens: SIGSTORE_ID_TOKEN: aud: https://your-provider-audience script: - # your build/deploy steps ``` * **Issuer URL:** `https://gitlab.com` (or your self-hosted instance) See [GitLab CI OIDC documentation](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html) for details. ### Fly.io [Section titled “Fly.io”](#flyio) OIDC tokens are available automatically to all Fly Machines. No additional configuration needed. * **Issuer URL:** `https://oidc.fly.io` See [Fly.io OIDC documentation](https://fly.io/docs/security/openid-connect/) for details. ### GCP Cloud Run [Section titled “GCP Cloud Run”](#gcp-cloud-run) Identity tokens are available via the GCP metadata server for services with a service account. * **Issuer URL:** `https://accounts.google.com` ### Custom / other platforms [Section titled “Custom / other platforms”](#custom--other-platforms) For platforms that aren’t auto-detected, pass an explicit OIDC JWT token using the `oidcToken` parameter on any supported plugin: ```env-spec # @initAws(region=us-east-1, oidcRoleArn="arn:aws:iam::123:role/my-role", oidcToken=$MY_OIDC_TOKEN) ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Token not detected [Section titled “Token not detected”](#token-not-detected) * **Vercel:** Ensure `VERCEL_OIDC_TOKEN` is available (check Vercel project settings) * **GitHub Actions:** Add `permissions: id-token: write` to your workflow/job * **GitLab CI:** Configure `id_tokens` in your job definition * **Other platforms:** Pass an explicit token via `oidcToken=...` ### Token exchange failed [Section titled “Token exchange failed”](#token-exchange-failed) * Verify the OIDC issuer URL matches exactly what you configured on the provider * Check that the audience claim matches what the provider expects * Ensure the subject/claims conditions match your deployment context ### Permission denied after successful auth [Section titled “Permission denied after successful auth”](#permission-denied-after-successful-auth) * The OIDC exchange succeeded, but the resulting identity doesn’t have the right permissions * Check role/policy assignments on the provider side # Plugins > Using plugins with varlock Plugins allow extending the functionality of Varlock. Specifically they may introduce new [root decorators](/reference/root-decorators/), [item decorators](/reference/item-decorators/), [data types](/reference/data-types/), and [resolver functions](/reference/functions/). 1Password plugin example ```env-spec # @plugin(@varlock/1password-plugin) # load + install plugin # @initOp(token=$OP_TOKEN, allowAppAuth=true) # init via custom root decorator # --- # @type=opServiceAccountToken # custom data type OP_TOKEN= # @sensitive XYZ_API_KEY=op(op://api-prod/xyz/api-key) # custom resolver function ``` This enables use cases like: * loading values from cloud providers or locally running services * adding domain-specific validation/coercion logic via custom data types * generating values dynamically via custom resolver functions Plugins are authored in TypeScript and can be loaded via local files, or from package registries like npm. Varlock will handle downloading and caching plugins automatically. ## Plugin installation [Section titled “Plugin installation”](#installation) Plugins are loaded using the `@plugin()` root decorator with an npm package name. How you specify the version depends on whether you are in a JavaScript project or using the standalone binary. ### JavaScript projects [Section titled “JavaScript projects”](#javascript-projects) In a JavaScript project (where a `package.json` file is present), you must install the plugin as a dependency in your project and load it **without** a version specifier. Varlock will use the version installed in your `node_modules` directory. .env.schema ```env-spec # @plugin(@varlock/a-plugin) # uses the locally installed version ``` You can optionally add a version specifier to validate that the installed version satisfies a [semver range](https://devhints.io/semver), but the local version is always what gets loaded. If the installed version doesn’t match the specified range, an error will be thrown. .env.schema ```env-spec # @plugin(@varlock/a-plugin@^2.3.4) # validates installed version is >=2.3.4 <3.0.0 ``` ### Standalone binary [Section titled “Standalone binary”](#standalone-binary) When using the standalone binary (no `package.json` present), you must specify a **fixed version number** (e.g., `1.2.3`). Semver ranges are not supported in this mode. Varlock will automatically download and cache the plugin from npm. .env.schema ```env-spec # @plugin(@varlock/a-plugin@1.2.3) # downloads and caches v1.2.3 from npm ``` Third-party plugin confirmation When downloading a third-party (non-`@varlock/*`) plugin for the first time, Varlock will prompt you to confirm before downloading. Once confirmed and cached, subsequent runs will not re-prompt. You can also install the plugin via your `package.json` to skip the prompt entirely. ## Plugin scope [Section titled “Plugin scope”](#plugin-scope) Plugins are loaded globally, and the additional functionality they provide will be available in all `.env` files in your project. Only a single `@plugin()` decorator is needed to load the plugin, even if multiple files use its functionality. If a plugin is loaded in multiple files, no error will be thrown, as long as they all use the same version. Note that plugins will not be loaded from an inactive file - for example an environment-specific file that does not match the current environment, or one that uses the [`@disable` root decorator](/reference/root-decorators/#disable). No specific namespacing or prefixes are enforced, and any naming conflicts will trigger an error, but plugins will use specific names to avoid conflicts. ## Initialization [Section titled “Initialization”](#initialization) Plugins are initialized using custom root decorators that they introduce. In some cases, no specific initialization is needed, and in others, you may need to initialize multiple instances of a plugin with different options, referred to by some identifier. How (or if) a plugin needs to be initialized depends on the specific plugin and can depend on the the external service’s data/auth model. A plugin initialization root decorator is used to set IDs, toggle features, and wire up auth. Note that sensitive data should be passed in via references to config items within your schema. Plugin initialization example ```env-spec # @initOp(account=acmeco, token=$OP_TOKEN, allowAppAuth=forEnv(dev)) # --- # @type=opServiceAccountToken @sensitive OP_TOKEN= ``` ### Multiple plugin instances [Section titled “Multiple plugin instances”](#multiple-plugin-instances) In secret storage tools, you should segment your data to follow the [*principle of least privilege*](https://en.wikipedia.org/wiki/Principle_of_least_privilege), so that different environments/services/devs only have access to the minimal secrets they need. At the very least, this usually means splitting your extra sensitive prod secrets from everything else, but it can be as fine-grained as needed. We cannot always assume that you won’t need access to multiple segments at the same time. In these cases, a plugin may be designed to be initialized multiple times with some kind of id parameter. Resolver functions and decorators can then accept an additional parameter to specify which instance to use. .env.schema ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(id=dev, token=$OP_TOKEN_DEV, allowAppAuth=forEnv(dev)) # @initOp(id=prod, token=$OP_TOKEN_PROD, allowAppAuth=false); # --- # @type=opServiceAccountToken @sensitive OP_TOKEN_DEV= # @type=opServiceAccountToken @sensitive OP_TOKEN_PROD= XYZ_API_KEY=op(dev, op://api-creds-dev/xyz/api-key) ``` .env.production ```env-spec XYZ_API_KEY=op(prod, op://api-creds-prod/xyz/api-key) ``` *While the 1Password plugin can be set up using a single instance (using a higher scoped service account for prod) you might want to use multiple instances if you want to make sure you don’t accidentally access prod secrets while working locally.* ## Usage [Section titled “Usage”](#usage) Once installed, all decorators, data types, and resolver functions provided by the plugin will be available for use within your `.env` files. These are available globally, and ordering is not important. Some decorators or resolver functions may require the plugin to be initialized and will throw an error if not set up properly. Please refer to the specific plugin’s documentation for details on usage. ## Plugin caching [Section titled “Plugin caching”](#plugin-caching) Plugins can use varlock’s built-in [cache](/guides/caching/) to avoid repeated API calls or hold short-lived tokens. Caching is opt-in - it is only active when the user enables it (typically via a `cacheTtl` init option) and the global cache mode allows it. Plugin authors get a scoped cache accessor on the plugin instance: ```ts // primary API - atomic get-or-set const value = await plugin.cache.getOrSet('some-key', '1h', () => fetchFromApi()); // lower-level helpers await plugin.cache.get('some-key'); await plugin.cache.set('some-key', value, '1h'); plugin.cache.delete('some-key'); ``` Keys are automatically prefixed with `plugin:{name}:`, so plugins cannot collide with each other’s cache entries. # Schema > Using the schema to manage your environment variables One of the core features of Varlock is its schema-driven approach to environment variables, which is best when shared with your team and committed to version control. We recommend creating a new `.env.schema` file to hold schema info set by [config item decorators](/reference/item-decorators/), non-sensitive default values, and [root decorators](/reference/root-decorators/) to specify global settings that affect the `varlock` CLI itself. This schema should include all of the environment variables that your application depends on, along with comments and documentation about them, and decorators which affect coercion, validation, and generated types / documentation. The more complete your schema is, the more validation and coercion `varlock` can perform, and the more it can help you catch errors earlier in your development cycle. Note Running [`varlock init`](/reference/cli-commands/#init) will attempt to convert an existing `.env.example` file into a `.env.schema` file. It must be reviewed, but it should be a good starting point. ## Root Decorators [Section titled “Root Decorators”](#root-decorators) The *header* section of a `.env` file is any comment block(s) at the beginning of the file, before the first config item. Within this header, you can use [root decorators](/reference/root-decorators/) to specify global settings and default behavior for all config items. .env.schema ```env-spec # This is the header, and may contain root decorators # @currentEnv=$APP_ENV # @defaultSensitive=false @defaultRequired=false # @generateTsTypes(path=env.d.ts) # --- # This is a config item comment block and may contain decorators which affect only the item # @required @type=enum(dev, test, staging, prod) APP_ENV=dev ``` More details: * [Root decorators reference](/reference/root-decorators/) ## Config Items [Section titled “Config Items”](#config-items) Config items are the environment variables that your application depends on. Like normal `.env` syntax, each item is a key-value pair of the form `KEY=value`. The key is the name of the environment variable, and a value may be specified or not. While simply enumerating all of them in your `.env.schema` is useful (like a `.env.example` file), [@env-spec](/env-spec/overview/) allows us to attach additional comments and [item decorators](/reference/item-decorators/), making our schema much more capable. ### Item Values [Section titled “Item Values”](#item-values) Values may be static, or set using [functions](/reference/functions/), which can facilitate loading values from external sources without exposing any sensitive values. **Quote rules:** * Static values can be wrapped in quotes or not — all quotes styles (`` ` ``, `"`, `'`) are supported * Values wrapped in single quotes do not support [expansion](#ref-expansion) * Single line values may not contain newlines, but `\n` will be converted to an actual newline except in single quotes * Multiline values can be wrapped in ` ``` `, `"""`. Also supported is `"` and `'` but not recommended. * Unquoted values will be parsed as a number/boolean/undefined where possible (`ITEM=foo` -> `"foo"`, while `ITEM=true` -> `true`), however data-types may further coerce values * No value (undefined) and empty string ("") are distinct .env.schema ```env-spec NO_VALUE= # will resolve to undefined EMPTY_STRING_VALUE="" # will resolve to empty string STATIC_VALUE_UNQUOTED=quotes are optional # but are recommended! STATIC_VALUE_QUOTED="#hashtag" # and are necessary in some cases BOOLEAN_VALUE=true NUMERIC_VALUE=123.456 FUNCTION_VALUE=op(op://api-config/item/credential) EXPANSION_VALUE=${OTHER_VAR}-suffix MULTILINE_VALUE=""" multiple lines """ ``` ### Item comments [Section titled “Item comments”](#item-comments) Comments are used to attach additional documentation and metadata to config items using [item decorators](/reference/item-decorators). This additional metadata is used by varlock to perform validation, coercion, and generate types / documentation. Multiple comment lines *directly* preceding an item will be attached to that item. A blank line or a divider (e.g., `# ---`) breaks a comment block and detaches it from the following config item. Any comment block before the first item is still part of the document header until it is ended by a blank line, a divider, or the end of the file. Comment lines can either contain regular comments or [item decorators](/reference/item-decorators). Standalone comments only count as decorator comments when the comment content starts with `@`. ```env-spec # description of item can be multiple lines # this @decorator will be ignored because the line does not start with @ # @sensitive=false @required # decorator lines can end with a comment # @type=string(startsWith=pk-) # multiple lines of decorators are allowed SERVICE_X_PUBLISHABLE_KEY=pk-abc123 ``` More details: * [Item decorators reference](/reference/item-decorators) * [@type data types reference](/reference/data-types) * [Functions reference](/reference/functions) ## Resolver Functions [Section titled “Resolver Functions”](#resolver-functions) You may use [resolver functions](/reference/functions/) instead of static values within both config items and decorator values. Functions may be composed together to create more complex value resolution logic. ```env-spec # @required=forEnv(prod) API_DOMAIN=if(eq(ref(APP_ENV), prod), api.myapp.com, staging-api.myapp.com) ``` ### Referencing other values [Section titled “Referencing other values”](#ref-expansion) Within values and function args, you often need to reference other env vars within your schema. You may use [`ref()`](/reference/functions/#ref) but we support *expansion* syntax (like many other .env tools) for convenience. Both `$ITEM` and `${ITEM}` are equivalent to `ref(ITEM)`. We recommend using the bracket version only when used within a larger string. ```env-spec WITH_BRACKETS=op(op://${OP_VAULT_NAME}/service/api-key) NO_BRACKETS=fallback($OTHERVAR, foo) ``` Read more about string expansion in the [@env-spec reference](/env-spec/reference/#expansion). ## Decorator details [Section titled “Decorator details”](#decorator-details) ### Functions vs single use [Section titled “Functions vs single use”](#functions-vs-single-use) Most decorators take a single value (e.g., `@sensitive`, `@currentEnv`) and may be used only once per item (or file in the case of a root decorator). Some decorators however, are function calls (e.g., `@docs()`, `@import()`) and may be called multiple times. ```env-spec # @sensitive=true # @docs(https://xyzapi.com/docs/auth) # @docs(https://xyzapi.com/manage-api-keys) XYZ_API_KEY= ``` ### Value resolution [Section titled “Value resolution”](#value-resolution) Values passed to decorators will be resolved, meaning if a decorator is expecting a boolean, either a static `true`/`false` or a [resolver function](/reference/functions) that resolves to a boolean may be used. ```env-spec # @required=false NEVER_REQUIRED= # @required=forEnv(prod) # resolves to true/false depending on the current environment REQUIRED_FOR_PROD= ``` # Secrets management > Best practices for managing secrets and sensitive environment variables with varlock Varlock uses the term *sensitive* to describe any value that should not be exposed to the outside world. This includes secret API keys, passwords, and other generally sensitive information. Instead of relying on prefixes (e.g., `NEXT_PUBLIC_`) to know which items may be “public”, varlock relies on `@decorators` to mark sensitive items explicitly. Because we understand which values are sensitive, we can apply extra security guardrails to keep them safe at every step of the SDLC. No more plaintext secrets! One of varlock’s main goals is to help you keep *every secret out of plaintext*. This reduces the risk of leaks via supply chain attacks, accidental commits to version control, LLM access, and other common pitfalls. ## Identifying `@sensitive` items [Section titled “Identifying @sensitive items”](#identifying-sensitive-items) Whether each item is sensitive or not is controlled by the [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive) root decorator and the [`@sensitive`](/reference/item-decorators/#sensitive) item decorator. For example: .env.schema ```env-spec # @defaultSensitive=false # --- # not sensitive by default (because of the root decorator) NON_SECRET_FOO= # @sensitive # explicitly marking this item as sensitive SECRET_FOO= ``` These decorators are typically used only in your `.env.schema` file, but may be used in any file, which can be useful to override sensitivity in a specific environment. ## Local encryption via `varlock()` [Section titled “Local encryption via varlock()”](#local-encryption) For local git-ignored secrets (typically overrides in `.env.local`), use the [`varlock()` function](/reference/functions/#varlock) to store encrypted values on disk. Note that the encryption key used is tied to your device, so these encrypted values are not meant to be shared or committed to git. .env.local ```diff(lang=env-spec) PLAINTEXT=shh-im-secret # 🚨 danger SECURED=varlock(local:abc123...) # ✅ secured at rest NEW_ITEM=varlock(prompt) # will prompt for new value ``` See the [local encryption guide](/guides/local-encryption/) for full setup, platform-specific details, and related CLI commands like [`varlock encrypt`](/reference/cli-commands/#encrypt), [`varlock reveal`](/reference/cli-commands/#reveal), and [`varlock lock`](/reference/cli-commands/#lock). On macOS, you can also use the built-in [keychain()](/plugins/macos-keychain/) plugin, which provides a similar experience but stores encrypted values in the system keychain. ## Loading secrets from external sources [Section titled “Loading secrets from external sources”](#loading-secrets-from-external-sources) ### Using plugins (recommended) [Section titled “Using plugins (recommended)”](#using-plugins-recommended) `varlock` provides official plugins for popular secret management platforms, giving you a type-safe way to fetch secrets directly in your `.env` files. Available plugins include: * [1Password](/plugins/1password/) * [AWS Secrets Manager & Parameter Store](/plugins/aws-secrets/) * [Azure Key Vault](/plugins/azure-key-vault/) * [Bitwarden](/plugins/bitwarden/) * [Google Secret Manager](/plugins/google-secret-manager/) * [HashiCorp Vault](/plugins/hashicorp-vault/) * [Infisical](/plugins/infisical/) See the [plugins overview](/plugins/overview/) for the complete list. Plugins are able to register new decorators and resolver functions that declaratively fetch secrets: ```env-spec # Install and initialize the 1Password plugin # @plugin(@varlock/1password-plugin) # @initOp(token=$OP_TOKEN, allowAppAuth=forEnv(dev)) # --- # Load secrets using the op() resolver function # @sensitive @required MY_SECRET=op(op://my-vault/item-name/field-name) ``` Benefits of using plugins: * Declarative secret references safe to check into version control * Built-in validation and type safety applied to fetched values * Built-in authentication handling * Better error messages and debugging * Platform-specific features (e.g., biometric unlock for 1Password) See each plugin’s documentation for detailed setup instructions. ### Using exec() as a fallback [Section titled “Using exec() as a fallback”](#using-exec-as-a-fallback) For cases where a plugin doesn’t exist or you need custom logic, `varlock` supports fetching secrets via CLI commands using `exec()` function syntax. ```env-spec # A secret fetched via a custom CLI / script # @sensitive @required MY_SECRET=exec(`./scripts/fetch-secret.sh prod/api-credential`) ``` This approach works with any CLI tool, ensuring no secrets are left in plaintext on your system, even if they are gitignored. If a [plugin](/plugins/overview/) already exists for your provider (1Password, AWS, Vault, …), prefer it over `exec()`. Plugin resolvers like `op()` handle auth and caching for you. ### Bulk injection with `@setValuesBulk()` [Section titled “Bulk injection with @setValuesBulk()”](#bulk-injection-with-setvaluesbulk) For some secret management platforms, you may already be setting key names that match your environment variable names - in which case, wiring up each value can feel like a lot of boilerplate. In cases like this, you can set many values at once using the [`@setValuesBulk()`](/reference/root-decorators/#setvaluesbulk) root decorator. For example, using 1Password, you could store a .env style blob within a text field, or you could fetch values from their new environments tool. .env.schema ```env-spec # fetch a dotenv style blob within a text field # @setValuesBulk(op("op://vault/item/field")) # # load values in a 1Password environment # @setValuesBulk(opLoadEnvironment(your-environment-id), createMissing=true) # # load all secrets from an Infisical project environment # @setValuesBulk(infisicalBulk()) # # load Infisical secrets filtered by path or tag # @setValuesBulk(infisicalBulk(path="/database", tag="backend")) # # load all key/value pairs from a HashiCorp Vault path # @setValuesBulk(vaultSecret("secret/myapp/config", raw=true)) # # inject only specific keys (pick allowlist), or everything except some (omit denylist) # both accept simple globs like API_* # @setValuesBulk(opLoadEnvironment(your-environment-id), pick=[API_KEY, DB_*]) # @setValuesBulk(infisicalBulk(), omit=[LEGACY_TOKEN]) ``` The bulk values are injected at the precedence level of the file containing the decorator, so `.env.local` and `process.env` will still override them as expected. See the [reference docs](/reference/root-decorators/#setvaluesbulk) for full details. ## Security enhancements [Section titled “Security enhancements”](#security-enhancements) Unlike other tools where you have to rely on pattern matching to detect *sensitive-looking* data, `varlock` knows exactly which values are sensitive, and can take extra precautions to protect them. ### CLI log redaction [Section titled “CLI log redaction”](#cli-log-redaction) The varlock CLI itself redacts sensitive values in its own output. Commands like [`varlock explain`](/reference/cli-commands/#explain) will display resolved values with partial masking (e.g., `my▒▒▒▒▒`) so you can verify configuration without exposing secrets in your terminal scrollback. When you need to see the actual plaintext value, use [`varlock reveal`](/reference/cli-commands/#reveal) which provides a secure viewing experience: ```bash varlock reveal # interactive picker to select a secret varlock reveal MY_SECRET # reveal a specific variable varlock reveal MY_SECRET --copy # copy to clipboard (auto-clears after 10s) ``` The `reveal` command displays values in an **alternate screen buffer**. When you press any key to dismiss, the value disappears from your terminal entirely and won’t be visible in scrollback history. With `--copy`, the value is copied to your clipboard and automatically cleared after 10 seconds. #### Redaction in `varlock run` [Section titled “Redaction in varlock run”](#redaction-in-varlock-run) When the output of [`varlock run`](/reference/cli-commands/#run) is piped or redirected (CI logs, files, `| tee`, etc.), the child process’s stdout/stderr is piped through the same redaction engine, so any sensitive values that end up in your application’s output will be masked automatically before they persist anywhere. When output is attached to an interactive terminal, it passes straight through instead. Piping would break interactive tools (e.g., `psql`, `claude`) that rely on TTY detection, and a human at the terminal already has access to the secrets. Use `--no-redact-stdout` to force-disable redaction for piped output, or `--redact-stdout` to force it (e.g., to override `@redactLogs=false`; errors if output is attached to an interactive terminal, where redaction is not possible without breaking TTY behavior): ```bash varlock run --no-redact-stdout -- node app.js | tee log.txt # force-disable redaction for piped output varlock run --redact-stdout -- node app.js > log.txt # force redaction despite @redactLogs=false ``` The same override can be set with the [`_VARLOCK_REDACT_STDOUT`](/reference/reserved-variables/#_varlock_redact_stdout) environment variable (`true`/`1` or `false`/`0`) when you can’t easily change the command, e.g. in a wrapper script or CI config. The CLI flag takes precedence over the env var. ### Runtime log redaction [Section titled “Runtime log redaction”](#runtime-log-redaction) *Only available in JavaScript/Node.js projects using varlock’s runtime integrations.* When using `varlock/auto-load` or a [framework integration](/integrations/overview/), varlock automatically patches global `console` methods (`log`, `warn`, `error`, `debug`, `info`, `trace`) to redact any sensitive values before they reach stdout/stderr. Sensitive values are replaced with a partially masked string using the `▒` character. For example, `my-secret-value` becomes `my▒▒▒▒▒`. ```js console.log(process.env.SECRET_KEY); // outputs "my▒▒▒▒▒" instead of "my-secret-value" ``` This works by intercepting Node.js internal console internals, with an additional layer that wraps the console methods themselves to handle environments where `console.log` has been patched by the platform (e.g., AWS Lambda). If you need to intentionally reveal a secret in logs (for example during debugging), you can use the `revealSensitiveConfig` helper: ```js import { revealSensitiveConfig } from 'varlock/env'; console.log(revealSensitiveConfig(process.env.SECRET_KEY)); // outputs the actual value ``` To disable runtime log redaction, set the [`@redactLogs`](/reference/root-decorators/#redactlogs) root decorator to `false`. ### Leak prevention [Section titled “Leak prevention”](#leak-prevention) *Only available in JavaScript/Node.js projects using varlock’s runtime integrations.* Varlock scans outgoing HTTP responses at runtime to detect if any sensitive values are being accidentally sent to clients. If a leak is detected, varlock throws an error with a detailed diagnostic message including the config item key and where the leak was detected. This works by patching: * **Node.js `ServerResponse`**: intercepts `write()` and `end()` calls, scanning text and JSON response bodies (including gzip-compressed responses) * **Global `Response` constructor**: intercepts the `Response` class used in edge runtimes (e.g., Cloudflare Workers), scanning bodies passed to the constructor and `Response.json()` Our [framework integrations](/integrations/overview/) automatically apply the appropriate patches for your environment. For example, a Next.js integration will scan both server-rendered pages and API route responses. To disable leak prevention, set the [`@preventLeaks`](/reference/root-decorators/#preventleaks) root decorator to `false`. Caution There is a potential performance impact for both `@preventLeaks` and `@redactLogs` when enabled, as they must inspect console output and response bodies. We feel they are beneficial enough to have on by default, but you can always opt out if needed. ## Scanning files for leaked secrets [Section titled “Scanning files for leaked secrets”](#scanning-files-for-leaked-secrets) The [`varlock scan` command](/reference/cli-commands/#scan) checks your project files for any plaintext occurrences of your `@sensitive` values. It loads your varlock config, resolves all sensitive values, and then searches through files to detect leaks. ```bash varlock scan ``` This is intended to be used as a pre-commit git hook to prevent accidentally committing secrets into version control. If no sensitive values are found in plaintext, it exits successfully. If any are detected, it reports the file, line number,and which secret was found, then exits with a non-zero status code. It can also be used to scan build output as an extra step to prevent accidentally bundling secrets into client-facing code. Our [drop-in integrations](/integrations/overview/) usually do this automatically, but this can be useful in some scenarios. ### Scanning modes [Section titled “Scanning modes”](#scanning-modes) * `varlock scan` - default mode, scans all files except gitignored ones * `varlock scan --include-ignored` - scans all files including gitignored ones * `varlock scan --staged` - scans only the files you have staged for commit * `varlock scan path1 path2/**/*.js` - scans specific files #### Automatic setup [Section titled “Automatic setup”](#automatic-setup) The easiest way to set this up is: ```bash varlock scan --install-hook ``` This will detect if you use a hook manager (like [husky](https://typicode.github.io/husky/) or [lefthook](https://github.com/evilmartians/lefthook)) and provide appropriate instructions. If no hook manager is detected, it will create a `.git/hooks/pre-commit` script for you. Note If varlock is installed as a project dependency (rather than a standalone binary), the generated hook command will automatically be prefixed with your package manager’s exec command (e.g., `npx varlock scan` or `bunx varlock scan`). #### Manual setup [Section titled “Manual setup”](#manual-setup) If you prefer to set it up yourself, add the following to your pre-commit hook: **Plain git hook** (`.git/hooks/pre-commit`): ```bash #!/bin/sh varlock scan ``` Make sure the hook file is executable: ```bash chmod +x .git/hooks/pre-commit ``` **With husky** (`.husky/pre-commit`): ```bash varlock scan ``` **With lefthook** (`lefthook.yml`): ```yaml pre-commit: commands: varlock-scan: run: varlock scan ``` Tip If you already have an existing pre-commit hook, just add `varlock scan` as an additional line in the script. It will exit with a non-zero code if any secrets are found, which will abort the commit. # Shell completion > Enable tab completion for the varlock CLI in bash, zsh, fish, and PowerShell The varlock CLI supports tab completion for subcommands and flags. There are two ways to get it, depending on how varlock is installed: * **`varlock` on your PATH** (standalone binary or global npm install): install varlock’s own completion script. See [Setup](#setup) below. * **varlock as a local project dependency** (run via `pnpm exec`, `npm exec`, `bun x`, scripts, etc.): completion comes from your package manager’s completion instead. See [Local project installs](#local-project-installs). ## Setup [Section titled “Setup”](#setup) Use this when `varlock` is on your PATH, installed via the [standalone binary](/getting-started/installation/#as-a-standalone-binary) (Homebrew or cURL) or a global npm install (`npm install -g varlock`). Generate a completion script with `varlock complete `, then install it using the instructions below for your shell. After upgrading varlock, re-run `varlock complete ` if new commands or flags were added. * Zsh Zsh completion files must be named with a leading underscore: ```bash mkdir -p ~/.zsh/completions echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc echo 'autoload -U compinit && compinit' >> ~/.zshrc varlock complete zsh > ~/.zsh/completions/_varlock source ~/.zshrc ``` * Bash ```bash mkdir -p ~/.local/share/bash-completion/completions varlock complete bash > ~/.local/share/bash-completion/completions/varlock source ~/.bashrc ``` * Fish ```bash mkdir -p ~/.config/fish/completions varlock complete fish > ~/.config/fish/completions/varlock.fish ``` Fish loads completions automatically. Restart your shell or open a new terminal to pick them up. * PowerShell ```powershell varlock complete powershell >> $PROFILE . $PROFILE ``` ## Local project installs [Section titled “Local project installs”](#local-project-installs) When varlock is only a local dependency (not on your PATH), you run it through your package manager: `pnpm exec varlock …`, `npm exec varlock …`, `bun x varlock …`, or a `package.json` script. In that case completion comes from **your package manager’s** completion, which delegates to varlock automatically. You don’t install anything varlock-specific; varlock already speaks the completion protocol that package-manager completion uses. Set up completion for your package manager once, using [`@bomb.sh/tab`](https://bomb.sh/docs/tab/#package-manager-completions): ```bash npm install -g @bomb.sh/tab # zsh (swap in npm / yarn / bun and bash / fish / powershell as needed) echo 'source <(tab pnpm zsh)' >> ~/.zshrc source ~/.zshrc ``` Now `pnpm exec varlock `, `pnpm dlx varlock `, and bare `pnpm varlock ` complete varlock’s subcommands and flags. The same works for `npm exec`, `yarn`, and `bun x`. `npx` / `bunx` aren’t covered Completion is registered against the package-manager binary (`npm`, `pnpm`, `yarn`, `bun`). `npx` and `bunx` are separate commands with no completion of their own, so `npx varlock ` / `bunx varlock ` won’t complete. Use `npm exec varlock` / `bun x varlock` instead. ## What gets completed [Section titled “What gets completed”](#what-gets-completed) Tab completion covers varlock **subcommands** and **static flags** (including enum choices like `load --format`). Dynamic values, such as env var names from your schema, are not completed yet. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) If completions stop working after an upgrade, regenerate the script: * npm ```bash npm exec -- varlock complete zsh > ~/.zsh/completions/_varlock ``` * pnpm ```bash pnpm exec -- varlock complete zsh > ~/.zsh/completions/_varlock ``` * bun ```bash bunx varlock complete zsh > ~/.zsh/completions/_varlock ``` * vlt ```bash vlx -- varlock complete zsh > ~/.zsh/completions/_varlock ``` * yarn ```bash yarn exec -- varlock complete zsh > ~/.zsh/completions/_varlock ``` * standalone binary ```bash varlock complete zsh > ~/.zsh/completions/_varlock ``` Replace `zsh` and the output path with your shell and install location. # Telemetry > Learn about varlock's anonymous usage analytics and how to opt out The `varlock` CLI collects **anonymous telemetry data** about usage to help us understand how the tool is being used and to make it better. Participation is optional, and you may opt-out at any time. ## What We Collect [Section titled “What We Collect”](#what-we-collect) We track general usage information, and the environment in which `varlock` is being used. Specifically we collect *anonymous* information about: * Which varlock command is being invoked * Which framework integration invoked the CLI (if any), such as Vite, Next.js, or Astro: official `@varlock/*` package name and version only * Which plugins are installed in the project: `@varlock/*` plugin names and versions are sent as-is; third-party plugin names are hashed (SHA-256) and versions are omitted * Anonymous, non-sensitive usage attributes that official `@varlock/*` plugins report about how they are used (for example which authentication mode is active, or whether a feature is enabled): booleans, short enum strings, and counts only; collected for official plugins only and strictly sanitized (never secret values, item references, hostnames, or paths) * Anonymous schema feature signals when a project is loaded: for example root settings like `@redactLogs`, cache mode, safe resolver/decorator identifiers in use, and data source type counts (not config keys, values, file paths, or error messages) * Whether the env graph was loaded (`graph_loaded`) and a coarse error category when the run failed (`error_code`: e.g. `parse_error`, `plugin_error`, `schema_error`, `validation_error`, `resolution_error`, `load_error`): no error messages or file paths * Version information for varlock and Node.js * JavaScript package manager in use (if detectable from lockfiles or runtime env): `npm`, `pnpm`, `yarn`, `bun`, or `deno` only * General system/machine information * Anonymous user + project ID **We will never collect any of your config files or environment variables.** ## How to Opt Out [Section titled “How to Opt Out”](#how-to-opt-out) You can opt out of analytics in three ways: ### Using the CLI [Section titled “Using the CLI”](#using-the-cli) Run the following command to permanently opt out: * npm ```bash npm exec -- varlock telemetry disable ``` * pnpm ```bash pnpm exec -- varlock telemetry disable ``` * bun ```bash bunx varlock telemetry disable ``` * vlt ```bash vlx -- varlock telemetry disable ``` * yarn ```bash yarn exec -- varlock telemetry disable ``` * standalone binary ```bash varlock telemetry disable ``` This will create/update a configuration file saving your preference at `$XDG_CONFIG_HOME/varlock/config.json` (defaults to `~/.config/varlock/config.json`). *You may re-enable telemetry by running `varlock telemetry enable`* ### Using an Environment Variable [Section titled “Using an Environment Variable”](#using-an-environment-variable) You can also opt out temporarily by setting the `VARLOCK_TELEMETRY_DISABLED` environment variable: ```bash export VARLOCK_TELEMETRY_DISABLED=true ``` This could be set in a specific terminal session, while running a specific command, in a Dockerfile, or in a CI/CD pipeline. ### With a project config file [Section titled “With a project config file”](#with-a-project-config-file) You can also opt out at the project level by creating a `.varlock/config.json` file in your project root with the following content: my-app/.varlock/config.json ```json { "telemetryDisabled": true } ``` ## Privacy [Section titled “Privacy”](#privacy) * All analytics data is completely anonymous * No personal or sensitive information is collected * Data is only used to improve the product * You can opt out at any time * Analytics are handled by [PostHog](https://posthog.com/), a privacy-friendly analytics platform ## Data Usage [Section titled “Data Usage”](#data-usage) The anonymous usage data helps us: * Understand which features are most used * Identify areas for improvement * Make informed decisions about future development * Prioritize bug fixes and new features If you have any questions about our analytics or privacy practices, please [start a discussion](https://github.com/dmno-dev/varlock/discussions) on GitHub. # Astro > How to integrate varlock with Astro for secure, type-safe environment management [![](https://img.shields.io/npm/v/@varlock/astro-integration?label=%40varlock%2Fastro-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/astro-integration) While Astro has [`astro:env`](https://docs.astro.build/en/guides/environment-variables/) to help with environment variables, we think Varlock has more to offer: * Your `.env.schema` is not tied to JavaScript, and is a better place to store this schema info versus your `astro.config.*` file * Facilitates loading and composing multiple `.env` files * You can use validated env vars right away within your `astro.config.*` file * Facilitates setting values and handling multiple environments, not just setting defaults * More data types and options available * Leak detection, log redaction, and more security guardrails To integrate varlock into an Astro application, you must use our [`@varlock/astro-integration`](https://npmx.dev/package/@varlock/astro-integration) package, which is an [Astro integration](https://docs.astro.build/en/guides/integrations-guide/). ## Setup [Section titled “Setup”](#setup) Requirements * Node.js v22 or higher * Astro v4 or higher 1. **Install varlock and the Astro integration package** * npm ```bash npm install @varlock/astro-integration varlock ``` * pnpm ```bash pnpm add @varlock/astro-integration varlock ``` * bun ```bash bun add @varlock/astro-integration varlock ``` * yarn ```bash yarn add @varlock/astro-integration varlock ``` * vlt ```bash vlt install @varlock/astro-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema` file** This will guide you through setting up your `.env.schema` file, based on your existing `.env` file(s). Make sure to review it carefully. * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Enable the Astro integration** You must add our `varlockAstroIntegration` to your `astro.config.*` file: astro.config.ts ```diff import { defineConfig } from 'astro/config'; +import varlockAstroIntegration from '@varlock/astro-integration'; export default defineConfig({ integrations: [varlockAstroIntegration(), otherIntegration()], }); ``` *** ## Accessing environment variables [Section titled “Accessing environment variables”](#accessing-environment-variables) You can continue to use `import.meta.env.SOMEVAR` as usual, but we recommend using varlock’s imported `ENV` object for better type-safety and improved developer experience: example.ts ```ts import { ENV } from 'varlock/env'; console.log(import.meta.env.SOMEVAR); // 🆗 still works console.log(ENV.SOMEVAR); // ✨ recommended ``` #### Why use `ENV` instead of `import.meta.env`? [Section titled “Why use ENV instead of import.meta.env?”](#why-use-env-instead-of-importmetaenv) * Non-string values (e.g., number, boolean) are properly typed and coerced * All non-sensitive items are replaced at build time (not just `VITE_` prefixed ones) * Better error messages for invalid or unavailable keys * Enables future DX improvements and tighter control over what is bundled ### Within `astro.config.*` [Section titled “Within astro.config.\*”](#within-astroconfig) It’s often useful to be able to access env vars in your Astro config. Without varlock, it’s a bit awkward, but varlock makes it easy. In fact it’s already available: import varlock’s `ENV` object and reference env vars via `ENV.SOME_ITEM` like you do everywhere else. astro.config.ts ```diff import { defineConfig } from 'astro/config'; import varlockAstroIntegration from '@varlock/astro-integration'; +import { ENV } from 'varlock/env'; +doSomethingWithEnvVar(ENV.FOO); export default defineConfig({ /* ... */ }); ``` TypeScript config If you find you are not getting type completion on `ENV`, you may need to add your generated type files (usually `env.d.ts`) to your `tsconfig.json`’s `include` array. ### Within other scripts [Section titled “Within other scripts”](#within-other-scripts) Even in a static front-end project, you may have other scripts in your project that rely on sensitive config. You can use [`varlock run`](/reference/cli-commands/#run) to inject resolved config into other scripts as regular env vars. * npm ```bash npm exec -- varlock run -- node ./script.js ``` * pnpm ```bash pnpm exec -- varlock run -- node ./script.js ``` * bun ```bash bunx varlock run -- node ./script.js ``` * vlt ```bash vlx -- varlock run -- node ./script.js ``` * yarn ```bash yarn exec -- varlock run -- node ./script.js ``` ### Type-safety and IntelliSense [Section titled “Type-safety and IntelliSense”](#type-safety-and-intellisense) To enable type-safety and IntelliSense for your env vars, enable the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) in your `.env.schema`. Note that if your schema was created using `varlock init`, it will include this by default. .env.schema ```diff +# @generateTsTypes(path='env.d.ts') # --- # your config items... ``` *** ## Managing multiple environments [Section titled “Managing multiple environments”](#managing-multiple-environments) Varlock can load multiple *environment-specific* `.env` files (e.g., `.env.development`, `.env.preview`, `.env.production`) by using the [`@currentEnv` root decorator](/reference/root-decorators/#currentenv). **This is different than Astro/Vite’s default behaviour, which relies on it’s own [`MODE` flag](https://vite.dev/guide/env-and-mode.html#modes).** Usually this env var will be defaulted to something like `development` in your `.env.schema` file, and you can override it by overriding the value when running commands - for example `APP_ENV=production vite build`. For a JavaScript based project, this will often be done in your `package.json` scripts. package.json ```json { "scripts": { "dev": "astro dev", "build": "APP_ENV=production astro build", "preview": "APP_ENV=production vite preview", } } ``` In some cases, you could also set the current environment value based on other vars already injected by your CI platform, like the current branch name. See the [environments guide](/guides/environments) for more information. ## Managing sensitive config values [Section titled “Managing sensitive config values”](#managing-sensitive-config-values) Astro uses the `PUBLIC_` prefix to determine which env vars are public (bundled for the browser). Varlock decouples the concept of being *sensitive* from key names, and instead you control this with the [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive) root decorator and the [`@sensitive`](/reference/item-decorators/#sensitive) item decorator. See the [secrets guide](/guides/secrets) for more information. Set a default and explicitly mark items: .env.schema ```diff +# @defaultSensitive=false # --- NON_SECRET_FOO= # sensitive by default # @sensitive SECRET_FOO= ``` Or if you’d like to continue using Astro’s prefix behavior: .env.schema ```diff +# @defaultSensitive=inferFromPrefix('PUBLIC_') # --- FOO= # sensitive PUBLIC_FOO= # non-sensitive, due to prefix ``` Bundling behavior All non-sensitive items are bundled at build time via `ENV`, while `import.meta.env` replacements continue to only include `PUBLIC_`-prefixed items. ### Leak Detection [Section titled “Leak Detection”](#leak-detection) This integration will automatically inject a new middleware that scans outgoing http responses for any sensitive values. Encrypting the env blob When deploying SSR Astro apps to serverless platforms, varlock injects the resolved env into your server-side build output as plaintext JSON. This is generally safe since it only appears in server code, but you can encrypt it for extra protection (e.g., against sourcemap leaks). See the [encrypted deployments guide](/guides/encrypted-deployments/) for setup instructions. *** ## Deploying to Cloudflare Workers [Section titled “Deploying to Cloudflare Workers”](#deploying-to-cloudflare-workers) If you deploy your SSR Astro app to Cloudflare Workers via [`@astrojs/cloudflare`](https://docs.astro.build/en/guides/integrations-guide/cloudflare/), install [`@varlock/cloudflare-integration`](/integrations/cloudflare/) alongside the Astro integration. The Astro integration **auto-detects** the Cloudflare adapter and wires up env injection into the worker. There’s no extra Vite or adapter config to write, and you keep using `astro dev` as normal. 1. **Install the Cloudflare integration** * npm ```bash npm install @varlock/cloudflare-integration ``` * pnpm ```bash pnpm add @varlock/cloudflare-integration ``` * bun ```bash bun add @varlock/cloudflare-integration ``` * yarn ```bash yarn add @varlock/cloudflare-integration ``` * vlt ```bash vlt install @varlock/cloudflare-integration ``` 2. **Use the Cloudflare adapter as usual** No varlock-specific Cloudflare setup is needed. Use the standard adapter alongside `varlockAstroIntegration()`: astro.config.ts ```ts import { defineConfig } from 'astro/config'; import cloudflare from '@astrojs/cloudflare'; import varlockAstroIntegration from '@varlock/astro-integration'; export default defineConfig({ output: 'server', adapter: cloudflare(), integrations: [varlockAstroIntegration()], }); ``` 3. **Deploy with `varlock-wrangler`** Use [`varlock-wrangler`](/integrations/cloudflare/#varlock-wrangler-cli) so your resolved env is uploaded as Cloudflare vars and secrets at deploy time: package.json ```json { "scripts": { "dev": "astro dev", "build": "astro build", "deploy": "astro build && varlock-wrangler deploy" } } ``` Requires `nodejs_compat` Varlock relies on a handful of `node:*` built-ins at runtime, so your worker must enable Cloudflare’s Node.js compatibility layer. Add `nodejs_compat` to `compatibility_flags` in your `wrangler.json` / `wrangler.toml`. Remove `.dev.vars` Varlock manages env injection automatically. Delete any `.dev.vars` file in your project root, or the dev server will error. See the [Cloudflare Workers integration docs](/integrations/cloudflare/) for full details on how dev injection, deployment, CI/CD, and `varlock-wrangler` work. *** ## Reference [Section titled “Reference”](#reference) * [Root decorators reference](/reference/root-decorators) * [Item decorators reference](/reference/item-decorators) * [Functions reference](/reference/functions) * [Astro’s environment variable docs](https://docs.astro.build/en/guides/environment-variables/) # Bun > How to integrate Varlock with a Bun-powered JavaScript project For the most part, Varlock just works with Bun the same way it works with Node.js, and other JavaScript integrations work the same way. ### Conflicts with Bun’s .env loading [Section titled “Conflicts with Bun’s .env loading”](#conflicts-with-buns-env-loading) Bun does its own automatic loading of `.env` files, based on the current value of `NODE_ENV` (or `BUN_ENV`), which it defaults to `development` if not set. This causes problems when bun decides to load `.env.development` and passes those env vars into varlock. The best way to fix this is to [disable bun’s automatic loading of `.env` files](https://bun.com/docs/runtime/environment-variables#disabling-automatic-env-loading) in your `bunfig.toml` file: bunfig.toml ```toml env = false ``` You may also use the `--no-env-file` CLI flag when invoking scripts with `bun`/`bunx`. Note that if you are building a standalone executable using `bun build`, you can use the `--no-compile-autoload-dotenv` flag to disable this behavior in the final executable. ### Using a preload script (optional) [Section titled “Using a preload script (optional)”](#using-a-preload-script-optional) One option we have with bun is to use a [preload script](https://bun.com/docs/runtime/bunfig#preload), configured in `bunfig.toml`. If you do this, you will no longer have to use `bun run varlock run -- yourscript` or use `import 'varlock/auto-load'` in your code! bunfig.toml ```toml preload = ["varlock/auto-load"] ``` Do not use preload with framework integrations Note that you should not do this if using a framework integration, as those integrations watch your `.env` files to trigger live-reloading. # Cloudflare Workers > How to integrate varlock with Cloudflare Workers and Wrangler for secure, type-safe environment management [![](https://img.shields.io/npm/v/@varlock/cloudflare-integration?label=%40varlock%2Fcloudflare-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/cloudflare-integration) Varlock manages environment variables in Cloudflare Workers with validation, type safety, and security features that go beyond Cloudflare’s built-in environment variable and secrets handling. Our integration relies on using `varlock-wrangler`, a thin wrapper around `wrangler` that automatically resolves and uploads your env vars as Cloudflare secrets and vars during deployment, and injects them into miniflare during local dev. Requires `nodejs_compat` Varlock relies on a handful of `node:*` built-ins at runtime, so your worker must opt into Cloudflare’s Node.js compatibility layer. Add `nodejs_compat` to your `compatibility_flags` in `wrangler.toml` / `wrangler.json`. Without it, the worker will fail to start with “Cannot resolve ‘node:…’” errors. wrangler.json ```json { "compatibility_flags": ["nodejs_compat"] } ``` ## Choosing an approach [Section titled “Choosing an approach”](#choosing-an-approach) All paths use the same `.env.schema` and `varlock-wrangler deploy` for production uploads. The difference is how env reaches your worker during **local dev** and **build**: | Approach | Best when | Dev / build | | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **[`varlock-wrangler`](#approach-1-using-varlock-wrangler)** | Plain Workers, minimal tooling | `varlock-wrangler dev` injects env via FIFO and watches `.env` files; `varlock-wrangler deploy` uploads vars + secrets | | **[Vite plugin](#approach-2-with-the-vite-plugin)** | Already using [`@cloudflare/vite-plugin`](https://developers.cloudflare.com/workers/vite-plugin/) | `vite dev` injects into miniflare automatically; `vite build` + `varlock-wrangler deploy` | | **[Astro adapter](/integrations/astro/#deploying-to-cloudflare-workers)** | Astro with [`@astrojs/cloudflare`](https://docs.astro.build/en/guides/integrations-guide/cloudflare/) | `@varlock/astro-integration` auto-detects the adapter; `astro dev` works as-is; `astro build` + `varlock-wrangler deploy` | | **[SvelteKit](/integrations/sveltekit/#setup-for-cloudflare-workers)** | SvelteKit with [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare) | `@varlock/vite-integration` auto-detects the adapter; `vite dev` via an SSR entry loader; `vite build` + `varlock-wrangler deploy` | Using SvelteKit? Follow the SvelteKit guide instead This page covers plain Workers and the `@cloudflare/vite-plugin` flow, which **does not support SvelteKit** ([workers-sdk#8922](https://github.com/cloudflare/workers-sdk/issues/8922)). For SvelteKit with `@sveltejs/adapter-cloudflare`, set up varlock using the **[SvelteKit → Cloudflare Workers guide](/integrations/sveltekit/#setup-for-cloudflare-workers)**. You use the standard `varlockVitePlugin` (which auto-detects the adapter), **not** `varlockCloudflareVitePlugin`. You’ll still install `@varlock/cloudflare-integration` (for `varlock-wrangler`) and deploy the same way described below. ## Approach 1: Using `varlock-wrangler` [Section titled “Approach 1: Using varlock-wrangler”](#approach-1-using-varlock-wrangler) Replace your `wrangler` commands with `varlock-wrangler` and initialize varlock in your worker code with a single import. It’s a thin wrapper that wires up your env vars correctly and passes everything else through unchanged. ### Setup [Section titled “Setup”](#setup) 1. **Install packages** * npm ```bash npm install @varlock/cloudflare-integration varlock ``` * pnpm ```bash pnpm add @varlock/cloudflare-integration varlock ``` * bun ```bash bun add @varlock/cloudflare-integration varlock ``` * yarn ```bash yarn add @varlock/cloudflare-integration varlock ``` * vlt ```bash vlt install @varlock/cloudflare-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema` file** This will guide you through setting up your `.env.schema` file, based on your existing `.env` file(s). Make sure to review it carefully. * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Add the varlock init import to your worker entry point** This initializes varlock’s `ENV` proxy and applies console redaction and response leak detection (unless disabled): src/index.ts ```diff +import '@varlock/cloudflare-integration/init'; +import { ENV } from 'varlock/env'; export default { async fetch(request, env, ctx) { +// both work: + console.log(ENV.MY_VAR); // varlock (recommended) + console.log(env.MY_VAR); // native Cloudflare return new Response('Hello!'); }, }; ``` 4. **Update your package.json scripts to use `varlock-wrangler`** Use `varlock-wrangler` as a drop-in replacement for `wrangler`: package.json ```json { "scripts": { "dev": "varlock-wrangler dev", "deploy": "varlock-wrangler deploy", "types": "varlock-wrangler types", } } ``` If you deploy via [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) instead of a local/CI script, override the deploy command in your Cloudflare dashboard under **Settings → Build → Deploy command** (see [Workers Builds](#cloudflare-workers-builds-cicd) below). ### How it works [Section titled “How it works”](#how-it-works) * **In dev**: `varlock-wrangler dev` resolves your env and injects it into `wrangler` using `--env-file` with a named pipe (unix/mac) or temp file (windows). It also watches your `.env` files and automatically restarts wrangler when they change, something `wrangler dev` doesn’t do. * **In production**: `varlock-wrangler deploy` (and `varlock-wrangler versions upload`) attaches non-sensitive values as Cloudflare vars (via `--var`) and sensitive values as Cloudflare secrets via `--secrets-file`. The worker reads them at runtime via `import { env } from 'cloudflare:workers'`. *** ## Approach 2: With the Vite plugin [Section titled “Approach 2: With the Vite plugin”](#approach-2-with-the-vite-plugin) If you’re building with Vite, `varlockCloudflareVitePlugin` wraps the [Cloudflare Workers Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) and adds env var + varlock init injection. No extra init import needed, and both `ENV.MY_VAR` and native `env.MY_VAR` work in dev and production. For SvelteKit on Cloudflare, use the standard [`varlockVitePlugin`](/integrations/sveltekit/#setup-for-cloudflare-workers) instead. It auto-detects the Cloudflare adapter and wires up the SSR entry loader. ### Setup [Section titled “Setup”](#setup-1) 1. **Install packages** * npm ```bash npm install @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * pnpm ```bash pnpm add @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * bun ```bash bun add @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * yarn ```bash yarn add @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * vlt ```bash vlt install @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` `@cloudflare/vite-plugin` is an (optional) peer dependency. This plugin wraps it, so you need it installed in the same project. SvelteKit users should omit it (see the [SvelteKit callout](#choosing-an-approach) at the top of this page). 2. **Run `varlock init` to set up your `.env.schema` file** * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Update your Vite config** Replace the `cloudflare()` plugin with `varlockCloudflareVitePlugin()`. It is a thin wrapper of the Cloudflare vite plugin and passes through all config: vite.config.ts ```diff import { defineConfig } from 'vite'; -import { cloudflare } from '@cloudflare/vite-plugin'; +import { varlockCloudflareVitePlugin } from '@varlock/cloudflare-integration'; export default defineConfig({ plugins: [ -cloudflare(), +varlockCloudflareVitePlugin(), // other plugins ... ], }); ``` Any options you were passing to `cloudflare()` can be passed directly to `varlockCloudflareVitePlugin()` at the top level. 4. **Deploy with `varlock-wrangler`** Use `varlock-wrangler deploy` instead of `wrangler deploy` in your deploy script: package.json ```json { "scripts": { "dev": "vite dev", "build": "vite build", "deploy": "npm run build && varlock-wrangler deploy" } } ``` If you deploy via [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) instead of a local/CI script, override the deploy command in your Cloudflare dashboard under **Settings → Build → Deploy command** (see [Workers Builds](#cloudflare-workers-builds-cicd) below). Remove `.dev.vars` If you have a `.dev.vars` file in your project root, **delete it**. Varlock manages env injection automatically. A `.dev.vars` file will conflict and the plugin will throw an error. This also applies to `.dev.vars.` files. ### How it works [Section titled “How it works”](#how-it-works-1) * **In dev**: Resolved vars are automatically injected into miniflare’s bindings. * **In production**: Non-sensitive values are statically replaced at build time by vite. `varlock-wrangler deploy` sets non-sensitive values as Cloudflare vars and sensitive values as secrets. ### Upgrading from `@varlock/vite-integration` [Section titled “Upgrading from @varlock/vite-integration”](#upgrading-from-varlockvite-integration) If you were previously using `varlockVitePlugin()` from `@varlock/vite-integration` with Cloudflare Workers: 1. Install `@varlock/cloudflare-integration` (you can remove `@varlock/vite-integration`, it’s included) 2. In `vite.config.ts`, replace both `varlockVitePlugin()` and `cloudflare()` with `varlockCloudflareVitePlugin()` 3. Replace `wrangler deploy` with `varlock-wrangler deploy` in your deploy script vite.config.ts ```diff -import { varlockVitePlugin } from '@varlock/vite-integration'; -import { cloudflare } from '@cloudflare/vite-plugin'; +import { varlockCloudflareVitePlugin } from '@varlock/cloudflare-integration'; export default defineConfig({ plugins: [ -varlockVitePlugin(), -cloudflare(), +varlockCloudflareVitePlugin(), ], }); ``` Your secrets will no longer be bundled into the JS artifact. They’ll be stored in Cloudflare’s secret management instead. *** ## `varlock-wrangler` CLI [Section titled “varlock-wrangler CLI”](#varlock-wrangler-cli) `varlock-wrangler` is a thin wrapper around `wrangler`. It enhances the commands below; everything else (`tail`, `secret list`, etc.) is passed through unchanged, so you can use `varlock-wrangler` everywhere or just for these commands. ### `varlock-wrangler dev` [Section titled “varlock-wrangler dev”](#varlock-wrangler-dev) Wraps `wrangler dev` with automatic env injection. Resolves your environment variables, injects them into miniflare, and watches your `.env` files to restart wrangler when they change. ### `varlock-wrangler deploy` / `versions upload` [Section titled “varlock-wrangler deploy / versions upload”](#varlock-wrangler-deploy--versions-upload) Resolves your environment variables from `.env.schema` + `.env` files, then uploads non-sensitive values as Cloudflare vars (`--var`), sensitive values as Cloudflare secrets (`--secrets-file`), and includes a `__VARLOCK_ENV` secret containing the full resolved env graph for the varlock runtime. Secure secret handling On Unix, secrets are passed to wrangler commands via a named pipe (FIFO) and never written to disk. On Windows, a temporary file is used and immediately deleted after the deploy completes. Varlock owns your env vars `varlock-wrangler deploy` replaces **all** Cloudflare vars and secrets with the ones defined in your `.env.schema`. Any vars or secrets set manually via the Cloudflare dashboard or `wrangler secret put` that aren’t in your schema will be removed on the next deploy. If you need additional bindings (KV, D1, etc.), configure them in your `wrangler.toml` as usual. Only plain vars/secrets are affected. ### `varlock-wrangler types` [Section titled “varlock-wrangler types”](#varlock-wrangler-types) Generates Cloudflare Worker types (the `Env` interface) including all varlock-managed environment variables, so your `env.MY_VAR` access is properly typed alongside other Cloudflare bindings (KV, D1, etc.). *** ## Log redaction and leak prevention [Section titled “Log redaction and leak prevention”](#log-redaction-and-leak-prevention) Both integration approaches automatically enable **log redaction** (sensitive values are masked in console output) and **response leak detection** (responses are scanned for accidentally exposed secrets). These are enabled by default and can be disabled in your `.env.schema` using root decorators: .env.schema ```env-spec # @redactLogs=false # @preventLeaks=false ``` For more details, see the [root decorators reference](/reference/root-decorators/). *** ## Managing Multiple Environments [Section titled “Managing Multiple Environments”](#managing-multiple-environments) Varlock can load multiple *environment-specific* `.env` files (e.g., `.env.development`, `.env.preview`, `.env.production`) by using the [`@currentEnv` root decorator](/reference/root-decorators/#currentenv). The simplest approach is to use the built-in [`VARLOCK_ENV`](/reference/builtin-variables/#varlock_env) variable, which auto-detects the deployment environment on most CI platforms, including [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) (via `WORKERS_CI_BRANCH`), Vercel, Netlify, and others. Locally it resolves to `development`, and during tests to `test`. .env.schema ```env-spec # @currentEnv=$VARLOCK_ENV # --- ``` If you need more control, for example mapping specific branches to specific environments, you can define your own env-selection var instead: .env.schema ```env-spec # @currentEnv=$APP_ENV # --- WORKERS_CI_BRANCH= # @type=enum(development, preview, production, test) APP_ENV=remap($WORKERS_CI_BRANCH, "main", production, /.*/, preview, undefined, development) ``` For more information, see the [environments guide](/guides/environments). *** ## Cloudflare Workers Builds (CI/CD) [Section titled “Cloudflare Workers Builds (CI/CD)”](#cloudflare-workers-builds-cicd) If you deploy via [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), two pieces of setup are required in the dashboard, and neither can be configured from a file in the repo: 1. **Override the Deploy command** Under **Settings → Build → Deploy command**, replace the default `npx wrangler deploy` with: ```sh npx varlock-wrangler deploy ``` Without this override, Cloudflare runs stock `wrangler deploy`, which skips varlock’s env resolution and leaves your worker without its resolved vars/secrets at runtime. 2. **Set any *secret-zero* vars under Build variables** Any env vars that varlock itself needs *during load* (for example a 1Password service account token or a GCP key) must be set under **Settings → Build → Variables and Secrets**. These are made available to the build step, where `varlock-wrangler deploy` resolves your full env graph and uploads the result to the worker runtime as regular vars/secrets. Vars that only your worker needs at runtime (not varlock itself) don’t need to be set here. They’ll be resolved by varlock and set automatically by `varlock-wrangler deploy`. *** ## Deploying from your own CI [Section titled “Deploying from your own CI”](#deploying-from-your-own-ci) Deploying from **your own CI** (GitHub Actions, GitLab CI, a self-hosted runner) instead of [Cloudflare Workers Builds](#cloudflare-workers-builds-cicd) works the same way, and isn’t really Cloudflare-specific. The same *secret-zero* rule applies: any var varlock needs to resolve your secrets (a 1Password service account token, a GCP key, etc.) must be present in the job environment before `varlock-wrangler deploy` runs. .github/workflows/deploy-worker.yml ```yaml jobs: deploy: permissions: id-token: write # required for OIDC-backed plugins contents: read steps: - uses: actions/checkout@v7 - run: npm ci - run: npm run build - run: npx varlock-wrangler deploy env: # Only vars varlock needs to *resolve* secrets, not every worker var OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} ``` For supported providers, varlock can authenticate with **short-lived OIDC tokens** from your CI instead of long-lived keys. See the [OIDC workload identity guide](/guides/oidc/). Run `varlock load` in an earlier step to fail fast on schema errors without exposing values. *** ## Multi-worker and auxiliary Workers [Section titled “Multi-worker and auxiliary Workers”](#multi-worker-and-auxiliary-workers) Cloudflare supports running [multiple Workers in one dev session](https://developers.cloudflare.com/workers/development-testing/multi-workers/): a primary Worker plus **auxiliary Workers** reachable via service bindings. The Vite plugin exposes this through [`auxiliaryWorkers`](https://developers.cloudflare.com/workers/development-testing/multi-workers/#vite-plugin-with-auxiliary-workers) in `vite.config.ts`. Single-worker env injection today Varlock currently resolves and uploads env vars for **one Wrangler config per deploy**. `varlock-wrangler deploy` injects vars/secrets into the worker named by the target `wrangler.toml` / `wrangler.json`. It does not automatically propagate env to auxiliary Workers or sibling configs passed to `wrangler dev -c ./a.json -c ./b.json`. For multi-worker repos today: * Deploy each Worker separately with `varlock-wrangler deploy -c `, or * Share a single `.env.schema` and run deploy once per worker script, or * Use [`varlock run`](/reference/cli-commands/#run) to inject resolved env when invoking a non-wrangler deploy tool (see [non-wrangler deploy tools](#non-wrangler-deploy-tools-alchemysstpulumi) below). First-class multi-worker / auxiliary-worker support is not implemented yet. If this blocks your setup, join [Discord](https://chat.dmno.dev) and share your wrangler layout and which Workers need which vars. *** ## When things go wrong [Section titled “When things go wrong”](#when-things-go-wrong) ### `Cannot resolve 'node:...'` at worker startup [Section titled “Cannot resolve 'node:...' at worker startup”](#cannot-resolve-node-at-worker-startup) Your worker is missing the Node.js compatibility layer. Add `nodejs_compat` to `compatibility_flags`; see the **`nodejs_compat` caution** near the top of this page. ### `.dev.vars` conflicts with the Vite plugin [Section titled “.dev.vars conflicts with the Vite plugin”](#devvars-conflicts-with-the-vite-plugin) Delete `.dev.vars` and `.dev.vars.` files when using the Vite plugin or `varlock-wrangler dev`. Varlock owns env injection; leftover Wrangler env files cause hard errors. ### Vars or secrets missing after deploy [Section titled “Vars or secrets missing after deploy”](#vars-or-secrets-missing-after-deploy) * Confirm you deploy with `varlock-wrangler deploy`, not stock `wrangler deploy`, especially on [Workers Builds](#cloudflare-workers-builds-cicd) * Check that secret-zero CI vars are set for any plugin-backed items varlock must resolve at deploy time * Remember that `varlock-wrangler deploy` **replaces all** Cloudflare vars and secrets with schema-defined ones. Manually added dashboard secrets not in your schema are removed ### Non-wrangler deploy tools (Alchemy, SST, Pulumi) [Section titled “Non-wrangler deploy tools (Alchemy, SST, Pulumi)”](#non-wrangler-deploy-tools-alchemy-sst-pulumi) If you deploy Workers with **IaC tools other than wrangler** ([Alchemy](https://alchemy.run/), SST, Pulumi, Terraform, etc.), you can still use varlock for **fail-fast validation and secret resolution** via [`varlock run`](/reference/cli-commands/#run), which injects resolved values into `process.env` for the deploy tool to read. What you **cannot** get today without `varlock-wrangler`: the in-worker runtime layer (`@varlock/cloudflare-integration/init`) that provides console redaction and response leak detection. That layer reads a `__VARLOCK_ENV` binding that only `varlock-wrangler` produces. A public API to build `__VARLOCK_ENV` bindings for non-wrangler deploy tools is planned but not available yet. Until then, teams on Alchemy/SST/Pulumi typically rely on varlock for validation + env injection at deploy time and skip the in-worker runtime protections. *** ## Varlock vs Cloudflare’s built-in env management [Section titled “Varlock vs Cloudflare’s built-in env management”](#varlock-vs-cloudflares-built-in-env-management) Cloudflare Workers have built-in support for [vars](https://developers.cloudflare.com/workers/configuration/environment-variables/) and [secrets](https://developers.cloudflare.com/workers/configuration/secrets/), but managing them across environments quickly becomes painful. Here’s what Varlock adds: * **Single source of truth**: Instead of scattering config across the Cloudflare dashboard, `wrangler.toml`, `.dev.vars`, and your code, everything is defined in one `.env.schema` file that works across local dev, preview, and production. * **Validation before deploy**: Cloudflare only has basic required var validation. Varlock catches missing vars, wrong types, and invalid values *before* your worker starts, not when it crashes at runtime. * **Pull secrets from vaults**: Instead of manually running `wrangler secret put` or copy-pasting values in the dashboard, pull secrets directly from [1Password](/plugins/1password/), [AWS Secrets Manager](/plugins/aws-secrets/), [HashiCorp Vault](/plugins/hashicorp-vault/), and [more](/plugins/overview/). * **Log redaction & leak prevention**: Cloudflare doesn’t prevent you from accidentally `console.log`-ing a secret or including one in a response body. Varlock automatically redacts sensitive values in logs and scans outgoing responses for leaked secrets. * **Simpler multi-environment setup**: Wrangler’s `[env.staging]` / `[env.production]` blocks duplicate config and don’t support `.env` file overrides. Varlock uses familiar `.env.production` / `.env.preview` files with a single schema, and auto-detects the current environment in CI. * **Local dev that matches production**: `.dev.vars` is completely disconnected from your production secrets. With Varlock, local dev resolves from the same schema and same secret sources, so there’s no drift between environments. * **AI-safe**: Your `.env.schema` gives AI coding tools full context on your config (names, types, descriptions) without ever exposing secret values. # direnv > Load validated environment variables into your shell with direnv and varlock [direnv](https://direnv.net/) is a shell extension that automatically loads and unloads environment variables when you enter and leave a directory. By combining it with varlock, you can get validated, schema-driven environment variables loaded directly into your shell session. Consider a deeper integration For projects where you control the codebase, using one of varlock’s [framework integrations](/integrations/overview/) or the [JavaScript / Node.js integration](/integrations/javascript/) is usually a better fit than direnv. Those integrations wire validation, type safety, and leak protection directly into your build and runtime, rather than relying on shell-level injection. You can also use [`varlock run`](/reference/cli-commands/#run) to inject env vars into any process without needing direnv. direnv is most useful when you need env vars available in your shell session itself, or when working with tools that cannot be launched via `varlock run`. ## How it works [Section titled “How it works”](#how-it-works) direnv works by executing a `.envrc` file in your project directory and capturing any exported variables into the current shell. Varlock’s `--format shell` flag outputs your resolved env vars as `export KEY=VALUE` lines that direnv can capture via `eval`. .envrc ```bash eval "$(varlock load --format shell)" ``` When you `cd` into your project, direnv runs this command and exports all your validated environment variables into the shell. ## Setup [Section titled “Setup”](#setup) 1. **Install direnv** Follow the [official direnv installation guide](https://direnv.net/docs/installation.html) for your platform and shell, then hook it into your shell profile. For example, for bash: ```bash echo 'eval "$(direnv hook bash)"' >> ~/.bashrc ``` 2. **Create a `.envrc` file** in your project root .envrc ```bash watch_file .env .env.* eval "$(varlock load --format shell)" ``` The `watch_file` line tells direnv to re-evaluate whenever any of your `.env` files change. Note that **new files added after the initial load will not be watched until you run `direnv reload`**, but this is rarely a concern since adding new env files is uncommon. Imported files outside the project directory If your `.env` files [import](/guides/import/) other files from outside the current directory, those external files will **not** be watched automatically. You would need to add additional `watch_file` entries for them, or run `direnv reload` manually after changing them. 3. **Allow the `.envrc` file** ```bash direnv allow ``` ## How it looks [Section titled “How it looks”](#how-it-looks) Once set up, when you `cd` into your project directory, direnv automatically loads your varlock-validated environment: ```bash $ cd my-project direnv: loading .envrc direnv: export +API_URL +DB_PASS +PORT ``` And when you leave the directory, those variables are automatically unloaded. ## Skip undefined values [Section titled “Skip undefined values”](#skip-undefined-values) By default, varlock outputs an empty assignment for undefined optional variables (e.g. `OPTIONAL_VAR=""`). If you prefer to skip undefined values entirely, use the `--compact` flag: .envrc ```bash watch_file .env .env.* eval "$(varlock load --format shell --compact)" ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) direnv strict mode Some shells or direnv configurations run `.envrc` files in strict mode (`set -euo pipefail`). If `varlock load` exits with a non-zero code (e.g. due to a validation error), direnv will halt and show an error. This is intentional: it prevents your shell from loading a broken environment. Loading errors If your environment fails validation, run `varlock load` directly in your terminal to see the full colorized output and error details before direnv loads it. # Expo / React Native > How to integrate varlock with Expo and React Native for secure, type-safe environment management [![](https://img.shields.io/npm/v/@varlock/expo-integration?label=%40varlock%2Fexpo-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/expo-integration) Expo and [React Native CLI](https://github.com/react-native-community/cli) projects both use the [Metro bundler](https://metrobundler.dev/), which has its own approach to environment variables. Varlock integrates via a **Babel plugin** that replaces `ENV.xxx` references with their resolved values at compile time, and a **Metro config wrapper** that initializes the `ENV` proxy at runtime. Both stacks use the same [`@varlock/expo-integration`](https://npmx.dev/package/@varlock/expo-integration) package. Only the Babel preset and Metro base config differ between Expo and React Native CLI projects. This integration does a few things: * Loads and validates your `.env` files using varlock at bundle time * Replaces `ENV.xxx` references to non-sensitive config items with their literal values at compile time * Sensitive values are **never** inlined into your bundle. In Expo projects they are accessible at runtime only in [Expo Router API routes](https://docs.expo.dev/router/reference/api-routes/) (`+api` files) * Patches the global console to redact sensitive values from logs No CLI command wrapping is needed. Env loads when Metro/Babel start. Run `expo start`, `npx react-native start`, or your usual build commands as normal. ## Setup [Section titled “Setup”](#setup) * Expo Requirements * Node.js v22 or higher * Expo SDK v50 or higher * React Native CLI Requirements * Node.js v22 or higher * React Native v0.72 or higher (ships `@react-native/metro-config`) 1. **Install varlock and the integration package** * npm ```bash npm install @varlock/expo-integration varlock ``` * pnpm ```bash pnpm add @varlock/expo-integration varlock ``` * bun ```bash bun add @varlock/expo-integration varlock ``` * yarn ```bash yarn add @varlock/expo-integration varlock ``` * vlt ```bash vlt install @varlock/expo-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema` file** This will guide you through setting up your `.env.schema` file, based on your existing `.env` file(s). Make sure to review it carefully. * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Add the Babel plugin to your `babel.config.js`** * Expo babel.config.js ```diff module.exports = { presets: ['babel-preset-expo'], + plugins: [ +require('@varlock/expo-integration/babel-plugin'), + ], }; ``` * React Native CLI babel.config.js ```diff module.exports = { presets: ['module:@react-native/babel-preset'], + plugins: [ +require('@varlock/expo-integration/babel-plugin'), + ], }; ``` 4. **Wrap your Metro config** Wrap your Metro config with `withVarlockMetroConfig`. This automatically configures Metro to resolve varlock’s subpath exports (e.g. `varlock/env`) and initializes the `ENV` proxy in the main Metro process. * Expo metro.config.js ```diff const { getDefaultConfig } = require('expo/metro-config'); +const { withVarlockMetroConfig } = require('@varlock/expo-integration/metro-config'); const config = getDefaultConfig(__dirname); -module.exports = config; +module.exports = withVarlockMetroConfig(config); ``` * React Native CLI metro.config.js ```diff const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withVarlockMetroConfig } = require('@varlock/expo-integration/metro-config'); const config = mergeConfig(getDefaultConfig(__dirname), {}); -module.exports = config; +module.exports = withVarlockMetroConfig(config); ``` What does this do? * Installs a custom resolver so that `import { ENV } from 'varlock/env'` works (Metro doesn’t support `package.json` `"exports"` subpaths by default). * Initializes the varlock `ENV` proxy in the main Metro process. Metro forks worker processes for Babel transforms; the wrapper ensures the environment is initialized in the main process (needed for Expo Router `+api` server routes and dev-time console redaction). *** ## Accessing environment variables [Section titled “Accessing environment variables”](#accessing-environment-variables) Rather than using `process.env.SOMEVAR` directly, use varlock’s `ENV` object for better type-safety: example.ts ```ts import { ENV } from 'varlock/env'; // Non-sensitive values are inlined at bundle time: const apiUrl = ENV.API_URL; // ✨ recommended, replaced at compile time // process.env still works, but loses type-safety and compile-time replacement: const legacyUrl = process.env.API_URL; // 🆗 still works ``` #### Why use `ENV` instead of `process.env`? [Section titled “Why use ENV instead of process.env?”](#why-use-env-instead-of-processenv) * Non-string values (e.g., number, boolean) are properly typed and coerced * Non-sensitive items are replaced with their literal values at bundle time (smaller bundle, no runtime lookup) * Sensitive items are never embedded in the bundle * Better error messages for invalid or unavailable keys ### How compile-time replacement works [Section titled “How compile-time replacement works”](#how-compile-time-replacement-works) When Metro compiles your project, the Babel plugin traverses the AST and replaces `ENV.xxx` member expressions: your-code.ts (before bundling) ```ts import { ENV } from 'varlock/env'; const url = ENV.API_URL; const port = ENV.PORT; const debug = ENV.DEBUG; ``` compiled output (after bundling) ```ts const url = "https://api.example.com"; const port = 3000; const debug = false; ``` Sensitive items (marked with `@sensitive` in your `.env.schema`) are **never** replaced. They remain as `ENV.xxx` references. In Expo Router `+api` server routes they resolve at runtime via the proxy; in all other code they throw at runtime. ### Type-safety and IntelliSense [Section titled “Type-safety and IntelliSense”](#type-safety-and-intellisense) To enable type-safety and IntelliSense for your env vars, enable the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) in your `.env.schema`. Note that if your schema was created using `varlock init`, it will include this by default. .env.schema ```diff +# @generateTsTypes(path='env.d.ts') # --- # your config items... ``` TypeScript config If you find you are not getting type completion on `ENV`, you may need to add your generated type files (usually `env.d.ts`) to your `tsconfig.json`’s `include` array. *** ## Managing multiple environments [Section titled “Managing multiple environments”](#managing-multiple-environments) Varlock can load multiple *environment-specific* `.env` files (e.g., `.env.development`, `.env.preview`, `.env.production`) by using the [`@currentEnv` root decorator](/reference/root-decorators/#currentenv). Usually this env var will be defaulted to something like `development` in your `.env.schema` file, and you can override it when running commands. * Expo For Expo projects, set it in your `package.json` scripts or via [EAS environment variables](https://docs.expo.dev/eas/environment-variables/): package.json ```json { "scripts": { "start": "expo start", "build:staging": "APP_ENV=staging eas build", "build:production": "APP_ENV=production eas build", } } ``` * React Native CLI For React Native CLI projects, set it in your `package.json` scripts: package.json ```json { "scripts": { "start": "react-native start", "start:staging": "APP_ENV=staging react-native start", "android:staging": "APP_ENV=staging react-native run-android", "ios:staging": "APP_ENV=staging react-native run-ios", } } ``` See the [environments guide](/guides/environments) for more information. ## Managing sensitive config values [Section titled “Managing sensitive config values”](#managing-sensitive-config-values) Sensitive values (marked with `@sensitive`) are **never** statically inlined into your bundle, regardless of any prefix conventions. You control this with the [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive) root decorator and the [`@sensitive`](/reference/item-decorators/#sensitive) item decorator. See the [secrets guide](/guides/secrets) for more information. Set a default and explicitly mark items: .env.schema ```diff +# @defaultSensitive=false # --- NON_SECRET_FOO= # non-sensitive - will be inlined into bundle # @sensitive SECRET_KEY= # sensitive - will NEVER be inlined, runtime only ``` Bundle security Non-sensitive items are inlined into your JavaScript bundle. Anyone who extracts your app’s bundle can read these values. **Never** mark credentials, tokens, or other secrets as non-sensitive. ### Sensitive values in server routes (Expo only) [Section titled “Sensitive values in server routes (Expo only)”](#sensitive-values-in-server-routes-expo-only) Expo Router only [Expo Router API routes](https://docs.expo.dev/router/reference/api-routes/) (`+api` files) are an **Expo-only** feature. React Native CLI apps have no server-side JS runtime, so skip to the next section. If you use Expo Router API routes (`+api` files), sensitive values **are** accessible at runtime via the `ENV` proxy. These files run server-side in the Metro process where `withVarlockMetroConfig` has initialized the environment. app/secret+api.ts ```ts import { ENV } from 'varlock/env'; export function GET() { // ✅ Sensitive values work in +api server routes const key = ENV.SECRET_KEY; return Response.json({ authorized: !!key }); } ``` ### Sensitive values in native code [Section titled “Sensitive values in native code”](#sensitive-values-in-native-code) In native app code (anything that isn’t an Expo Router `+api` server route), sensitive values are **not available**. React Native apps run entirely on the device, so there is no server to keep secrets safe. Accessing a sensitive value in native code will **throw at runtime**. The Babel plugin also emits a **build-time warning** when it detects a sensitive `ENV.xxx` reference in a non-server file, helping you catch these issues early. This applies to **all** React Native code, including React Native CLI projects where every screen and component is client-side. src/screens/Home.tsx ```ts import { ENV } from 'varlock/env'; const url = ENV.API_URL; // ✅ non-sensitive, inlined at build time const key = ENV.SECRET_KEY; // ❌ throws at runtime, build-time warning ``` Expo pages are universal Unlike Next.js Server Components, Expo Router page components run on **both** the server (SSR) and the client (hydration). This means you cannot safely access sensitive values in page components. Use `+api` server routes instead. *** ## Reference [Section titled “Reference”](#reference) * [Root decorators reference](/reference/root-decorators) * [Item decorators reference](/reference/item-decorators) * [Functions reference](/reference/functions) * [Expo environment variables docs](https://docs.expo.dev/guides/environment-variables/) * [Expo Router API routes](https://docs.expo.dev/router/reference/api-routes/) * [React Native Metro docs](https://reactnative.dev/docs/metro) * [React Native Community CLI](https://github.com/react-native-community/cli) * [Metro bundler docs](https://metrobundler.dev/) # GitHub Actions > Use Varlock in GitHub Actions to securely load and validate environment variables [![GitHub Actions Marketplace](https://img.shields.io/badge/GitHub%20Actions-Marketplace-blue?logo=github)](https://github.com/marketplace/actions/varlock-environment-loader) The Varlock GitHub Action loads and validates environment variables in your GitHub Actions workflows. It automatically detects and loads your `.env.schema` file and all relevant `.env.*` files, validates all environment variables against your schema, and exports them as either environment variables or a JSON blob for use in subsequent steps. ## Features [Section titled “Features”](#features) * 🔒 **Schema Validation**: Validates all environment variables against your `.env.schema` file * 🚀 **Auto-installation**: Automatically installs varlock if not present * 🔍 **Smart Detection**: Automatically loads `.env` and relevant `.env.*` files * 🛡️ **Security**: Handles sensitive values as GitHub secrets * 📊 **Flexible Output**: Export as environment variables or JSON blob .env.schema not required While you are encouraged to create a `.env.schema` file while using varlock, you can still use this GitHub Action without one. If you do not have a `.env.schema` file, the action will only load `.env`, since we won’t know what to use as your [environment flag](/guides/environments), and therefore which other `.env.*` files to load. ## Setup [Section titled “Setup”](#setup) 1. **Create or update your `.env.schema` file** Make sure you have a `.env.schema` file in your repository that defines your environment variables and their validation rules. .env.schema ```env-spec # @currentEnv=$APP_ENV # @defaultSensitive=false @defaultRequired=false # @generateTsTypes(path='env.d.ts') # --- # Environment flag # @type=enum(development, staging, production) APP_ENV=development # Database configuration # @type=url @required DATABASE_URL= # API configuration # @type=string(startsWith=sk-) @sensitive API_KEY= # Feature flags # @type=boolean ENABLE_FEATURE_X=false ``` 2. **Add the action to your workflow** .github/workflows/deploy.yml ```yaml name: Deploy Application on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v7 - name: Load environment variables uses: dmno-dev/varlock-action@v1.0.1 ``` ## Inputs [Section titled “Inputs”](#inputs) | Input | Description | Required | Default | | ------------------- | ---------------------------------------------- | -------- | ------- | | `working-directory` | Directory containing `.env.schema` files | No | `.` | | `show-summary` | Show a summary of loaded environment variables | No | `true` | | `fail-on-error` | Fail the action if validation errors are found | No | `true` | | `output-format` | Output format: `env` or `json` | No | `env` | ## Outputs [Section titled “Outputs”](#outputs) | Output | Description | | ------------- | ---------------------------------------------------------------------------------------------- | | `summary` | Summary of loaded environment variables using `varlock load` | | `error-count` | Number of validation errors found | | `json-env` | JSON blob containing all environment variables (only available when `output-format` is `json`) | ## Usage Examples [Section titled “Usage Examples”](#usage-examples) ### Basic Environment Variable Loading [Section titled “Basic Environment Variable Loading”](#basic-environment-variable-loading) This example loads environment variables and exports them for use in subsequent steps: .github/workflows/basic.yml ```yaml name: Basic Environment Loading on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v7 - name: Load environment variables uses: dmno-dev/varlock-action@v1.0.1 - name: Use environment variables run: | echo "Database URL: $DATABASE_URL" echo "API Key: $API_KEY" echo "Environment: $APP_ENV" ``` ### JSON Output Format [Section titled “JSON Output Format”](#json-output-format) Use JSON output when you need to reuse environment variables in multi-job workflows or pass them to other tools: .github/workflows/json-output.yml ```yaml name: JSON Output Example on: push: branches: [main] jobs: load-env: runs-on: ubuntu-latest outputs: env-vars: ${{ steps.varlock.outputs.json-env }} steps: - name: Checkout code uses: actions/checkout@v7 - name: Load environment variables as JSON id: varlock uses: dmno-dev/varlock-action@v1.0.1 with: show-summary: false output-format: 'json' build: needs: load-env runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v7 - name: Process environment variables run: | # Access the JSON blob from the previous job echo '${{ needs.load-env.outputs.env-vars }}' > env-vars.json # Use jq to process the JSON echo "Database URL: $(jq -r '.DATABASE_URL' env-vars.json)" echo "API Key: $(jq -r '.API_KEY' env-vars.json)" - name: Build application run: | # Use environment variables from JSON in build process DATABASE_URL=$(jq -r '.DATABASE_URL' env-vars.json) API_KEY=$(jq -r '.API_KEY' env-vars.json) echo "Building with DATABASE_URL: $DATABASE_URL" echo "Building with API_KEY: $API_KEY" # Your build logic here deploy: needs: build runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v7 - name: Deploy with environment variables run: | # Access the same environment variables from the first job echo '${{ needs.load-env.outputs.env-vars }}' > env-vars.json # Use environment variables in deployment DATABASE_URL=$(jq -r '.DATABASE_URL' env-vars.json) API_KEY=$(jq -r '.API_KEY' env-vars.json) echo "Deploying with DATABASE_URL: $DATABASE_URL" echo "Deploying with API_KEY: $API_KEY" # Your deployment logic here ``` ### Multi-Environment Workflows [Section titled “Multi-Environment Workflows”](#multi-environment-workflows) Handle different environments based on branch or deployment context: .github/workflows/multi-env.yml ```yaml name: Multi-Environment Deployment on: push: branches: [main, staging, develop] jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v7 - name: Load environment variables uses: dmno-dev/varlock-action@v1.0.1 env: # Set environment-specific values APP_ENV: ${{ github.ref_name == 'main' && 'production' || github.ref_name == 'staging' && 'staging' || 'development' }} - name: Deploy to environment run: | echo "Deploying to $APP_ENV environment" # Your deployment logic here ``` ## Error Handling [Section titled “Error Handling”](#error-handling) The action provides error handling and reporting: ### Validation Errors [Section titled “Validation Errors”](#validation-errors) When environment variables fail validation, the action will: 1. **Show detailed error messages** in the action logs 2. **Set the `error-count` output** with the number of errors found 3. **Fail the action** if `fail-on-error` is set to `true` (default) .github/workflows/error-handling.yml ```yaml name: Error Handling Example on: push: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v7 - name: Load environment variables id: varlock uses: dmno-dev/varlock-action@v1.0.1 with: fail-on-error: false # Don't fail on validation errors - name: Handle validation errors if: steps.varlock.outputs.error-count > '0' run: | echo "Found ${{ steps.varlock.outputs.error-count }} validation errors" echo "Check the varlock output above for details" # Your error handling logic here ``` ## Security Considerations [Section titled “Security Considerations”](#security-considerations) ### Sensitive Data Handling [Section titled “Sensitive Data Handling”](#sensitive-data-handling) The action automatically detects sensitive values based on your `.env.schema` configuration and handles them securely: * **Sensitive values** are exported as GitHub secrets available in the current workflow run * **Non-sensitive values** are exported as regular environment variables * **All values** are available in subsequent steps, but sensitive ones are masked in logs ### Environment Variable Scope [Section titled “Environment Variable Scope”](#environment-variable-scope) * Environment variables are only available within the job where the action runs * They are not persisted across jobs or workflow runs * Use the `json-env` output if you need to pass values between jobs, keeping in mind that this could possibly leak sensitive data if not handled correctly. You can also re-run the varlock action in a subsequent job to get the latest values. ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Always use `.env.schema` and \`.env.**\*: Define your environment structure and validation rules, see [environments guide](/guides/environments) for more information. 2. **Set `fail-on-error: true` (default)**: Catch configuration issues early in your CI/CD pipeline 3. **Handle errors gracefully**: Check the `error-count` output and provide meaningful feedback 4. **Secure sensitive data**: Mark sensitive values in your schema and let the action handle them securely ## Related Documentation [Section titled “Related Documentation”](#related-documentation) * [Environment Variables Guide](/guides/environments/) - Learn about managing multiple environments * [Schema Reference](/reference/root-decorators/) - Understand schema decorators and validation * [Getting Started](/getting-started/introduction/) - Set up Varlock in your project * [CLI Reference](/reference/cli-commands/) - Command-line interface documentation # Go > Use varlock with Go via a generated env package To use varlock with Go, [install the standalone binary](/getting-started/installation/#as-a-standalone-binary) and run your app under [`varlock run`](/reference/cli-commands/#run). It loads and validates your env, then injects it into the process. Add [`@generateGoEnv`](/reference/root-decorators/#generategoenv) to your schema to generate a small, self-contained package: an `Env` struct, a `Load()` function that parses the injected [`__VARLOCK_ENV`](/reference/reserved-variables/#__varlock_env) blob, and a `SensitiveKeys` map. .env.schema ```env-spec # @generateGoEnv(path=env/env.go) ``` The package name follows the output directory (`env/env.go` → `package env`); override it with `package=`. The file is regenerated automatically on [`varlock load`](/reference/cli-commands/#load) and [`varlock run`](/reference/cli-commands/#run), or explicitly with [`varlock codegen`](/reference/cli-commands/#codegen). Requires **Go 1.18+** (the generated code uses `any`). ## Reading values [Section titled “Reading values”](#reading-values) Call `Load()` once, then read fields off the struct. Required fields are the plain type, optional ones are pointers (deref when set): ```go package main import ( "log" "myapp/env" ) func main() { e, err := env.Load() // call once, hold or pass `e` around if err != nil { log.Fatal(err) } host := e.DbHost // string (required field) if e.DbPort != nil { // *int64 (optional field, deref when set) log.Printf("%s:%d", host, *e.DbPort) } } ``` ```bash varlock run -- go run . ``` `Load()` returns a clear error if `__VARLOCK_ENV` is missing (e.g. you forgot `varlock run`) or if a required key is absent. ## Sharing one instance [Section titled “Sharing one instance”](#sharing-one-instance) Every `Load()` call re-parses the blob. To avoid reloading, expose it as a package-level var in your own package: config/config.go ```go package config import "myapp/env" var Env = mustLoad() func mustLoad() env.Env { e, err := env.Load() if err != nil { panic(err) } return e } ``` anywhere.go ```go import "myapp/config" port := config.Env.DbPort // *int64 ``` Note Every key is also injected as a plain environment variable, so you can read any of them with `os.Getenv` (raw strings) if you don’t need the typed module. Enums become plain `string` (Go has no enum type), and keys whose names aren’t valid identifiers (they contain `.` or `-`) can’t be struct fields, so read those from the env var directly. See [Other languages](/integrations/other-languages/) for the shared details on the injected blob, raw env vars, and encryption. # JavaScript / Node.js > How to integrate Varlock with JavaScript and Node.js for secure, type-safe environment management There are a few different ways to integrate Varlock into a JavaScript / Node.js application. Some tools/frameworks may require an additional package, or have more specific instructions. Check the Integrations section in the navigation for more details. **Want to help us build more integrations? Join our [Discord](https://chat.dmno.dev)!** ## Node.js - `varlock/auto-load` [Section titled “Node.js - varlock/auto-load”](#nodejs---varlockauto-load) The best way to integrate varlock into a plain Node.js application (⚠️ version 22 or higher) is to import the `varlock/auto-load` module. This uses `execSync` to call out to the varlock CLI, sets resolved env vars into `process.env`, and initializes varlock’s runtime code, including: * varlock’s `ENV` object * log redaction (if enabled) * leak detection (if enabled) example-index.js ```js import 'varlock/auto-load'; import { ENV } from 'varlock/env'; const FROM_VARLOCK_ENV = ENV.MY_CONFIG_ITEM; // ✨ recommended const FROM_PROCESS_ENV = process.env.MY_CONFIG_ITEM; // 🆗 still works ``` dotenv drop-in replacement If you are using [`dotenv`](https://www.npmjs.com/package/dotenv), or a package you are using is using it under the hood, you can swap in varlock using your package manager’s override feature. See the [migrate from dotenv](/guides/migrate-from-dotenv) guide for more information. ## Boot via `varlock run` [Section titled “Boot via varlock run”](#boot-via-varlock-run) A less invasive way to use varlock with your application is to run your application via [`varlock run`](/reference/cli-commands/#run). ```bash varlock run -- ``` This will load and validate your environment variables, then run the command you provided with those environment variables injected into the process. This will not inject any runtime code, and varlock’s `ENV` object will not be available. If you have installed varlock as a project dependency instead of globally, you should run this via your package manager: * npm ```bash npm exec -- varlock run -- ``` * pnpm ```bash pnpm exec -- varlock run -- ``` * bun ```bash bunx varlock run -- ``` * vlt ```bash vlx -- varlock run -- ``` * yarn ```bash yarn exec -- varlock run -- ``` In `package.json` scripts, calling `varlock` directly will work, as your package manager handles path issues: package.json ```json "scripts": { "start": "varlock run -- node index.js" } ``` Even when using a deeper integration for your code, you may still need to use `varlock run` when calling external scripts/tools, like database migrations, to pass along resolved env vars. Setting the current environment Varlock can load multiple environment-specific `.env` files (e.g., `.env.development`, `.env.production`) by using the [`@currentEnv` root decorator](/reference/root-decorators/#currentenv) to specify which env var will set the current environment. If not using the default (usually `development`), you’ll want to pass it in as an environment variable when running your command. ```bash APP_ENV=production varlock run -- node index.js ``` See the [environments guide](/guides/environments) for more information about how to set the current environment. ## Front-end frameworks [Section titled “Front-end frameworks”](#front-end-frameworks) While environment variables are not available in the browser, many frameworks expose some env vars that are available *at build time* to the client by embedding them into your bundled code. This is best accomplished using tool-specific integrations, especially for frameworks that are handling both client and server-side code. Isomorphic env vars The `varlock/env` module is designed to be imported on both the client and server, so frameworks that run code in both places (like Next.js) can import it. Help us build more integrations! If you are using a tool/framework that is not listed here, and you’d like to see support for it, or collaborate on building it, we’d love to hear from you. Please hop into our [Discord](https://chat.dmno.dev)! # mise > Install varlock with mise and wire validated env vars into your tasks [mise](https://mise.jdx.dev/) (mise-en-place) is a polyglot tool manager, environment manager, and task runner. You can use it to install the `varlock` CLI itself, and to wire varlock into the commands you run during development. Inject into commands, not your shell mise can load environment variables into your shell session (via `[env]`), but we **don’t recommend** loading your varlock-managed env (especially secrets) that way. Once values live in your shell, every process inherits them, they show up in `env`, and varlock’s [log redaction](/guides/secrets/#redaction-in-varlock-run) is bypassed. Instead, integrate varlock at the point where each program runs: either by wrapping the command with [`varlock run`](/reference/cli-commands/#run) (works for anything), or with one of varlock’s [framework/runtime integrations](/integrations/overview/) (for JS/TS apps). Both approaches let you load env vars in different configurations per context (a different [environment](/guides/environments/) per command, build-time vs. runtime resolution, redacted output, leak protection) rather than forcing one global set into the shell where everything sees the same thing. See [Loading into your shell](#loading-env-vars-into-your-shell-advanced) for the narrow cases where shell injection still makes sense. ## Install varlock with mise [Section titled “Install varlock with mise”](#install-varlock-with-mise) mise can install and version-manage the `varlock` CLI alongside the rest of your toolchain. Already using varlock in a JS project? If varlock is already a dependency of your JavaScript/TypeScript project, you **don’t need any mise integration to install it**. Keep installing it with your package manager (`npm`/`pnpm`/`yarn`/`bun`) and run it via `npx varlock` / `bun run varlock` as usual. mise’s job there is managing your runtime (e.g. `node` or `bun`) and, optionally, [running your scripts as tasks](#running-your-commands-as-tasks). The backends below are for installing the standalone `varlock` CLI itself, useful for non-JS projects, or for sharing one varlock version across a team’s machines. * Standalone binary (recommended) The `github` backend installs varlock’s prebuilt, self-contained binary directly from our GitHub releases. It needs **no runtime** (no Node or Bun) and mise verifies the download’s checksum and build attestations automatically. mise.toml ```toml [tools] "github:dmno-dev/varlock" = "latest" ``` ```bash mise install varlock --version ``` Or try it without a config file: ```bash mise exec github:dmno-dev/varlock@latest -- varlock --version ``` `latest` can lag a fresh release by \~24h mise applies a default 24-hour `minimum_release_age` delay to releases as a supply-chain safeguard, so a brand-new version may be temporarily hidden from `latest`. To get a release immediately, pin the exact version: mise.toml ```toml [tools] "github:dmno-dev/varlock" = "1.7.0" ``` * npm package The `npm` backend installs varlock from npm. This **requires a Node.js runtime**, since mise uses Node’s `npm` under the hood, so declare `node` in the same `[tools]` table (mise will not provide it automatically): mise.toml ```toml [tools] node = "lts" "npm:varlock" = "latest" ``` ```bash mise install varlock --version ``` Prefer the standalone binary above when you only need the CLI. It avoids the Node dependency and starts faster. Using Bun? mise’s `npm` backend can’t be driven by Bun alone (it still needs Node for `npm`). If your project uses Bun or Node already, you typically wouldn’t install varlock as a mise *tool* at all. Add it as a project dependency and run it through [tasks](#running-your-commands-as-tasks) with your project’s runtime. See the [Bun](/integrations/bun/) and [JavaScript / Node.js](/integrations/javascript/) integrations. ## Running your commands as tasks [Section titled “Running your commands as tasks”](#running-your-commands-as-tasks) [mise tasks](https://mise.jdx.dev/tasks/) are a great place to run your project’s commands. Whether you need to mention varlock in the task depends on whether the command already loads it. ### When varlock is already wired into the command [Section titled “When varlock is already wired into the command”](#when-varlock-is-already-wired-into-the-command) Often it is. A `package.json` script may already call [`varlock run`](/reference/cli-commands/#run), or your app may use a [framework/runtime integration](/integrations/overview/) (Next.js, Vite, Astro, Node, etc.) that loads and validates env on startup. In that case the mise task just runs the command as-is, and nothing varlock-specific belongs here: mise.toml ```toml [tasks.dev] run = "npm run dev" # e.g. "dev": "varlock run -- next dev", or the app uses an integration ``` ```bash mise run dev ``` ### When the command doesn’t load varlock on its own [Section titled “When the command doesn’t load varlock on its own”](#when-the-command-doesnt-load-varlock-on-its-own) For a raw binary, a shell script, or a one-off command that isn’t already varlock-aware, wrap it with `varlock run` so it gets a resolved, validated environment with redacted output, scoped to that one subprocess, never your shell: mise.toml ```toml [tools] "github:dmno-dev/varlock" = "latest" # makes `varlock` available to the task [tasks.migrate] run = "varlock run -- ./scripts/migrate.sh" [tasks.psql] run = "varlock run -- psql" ``` Declaring varlock in `[tools]` lets mise install it and put it on `PATH` before the task runs. If varlock is instead a dependency of your JS project, call the project-local version through your package manager and drop it from `[tools]`, e.g. `run = "npx varlock run -- ./scripts/migrate.sh"` or `run = "bun run varlock run -- ./scripts/migrate.sh"`. ## Loading env vars into your shell (advanced) [Section titled “Loading env vars into your shell (advanced)”](#loading-env-vars-into-your-shell-advanced) Sometimes you genuinely want a value available to any command you type in the terminal, typically **non-secret** config like `NODE_ENV` or a public base URL. You can do this without a plugin using mise’s `env._.source`, which captures variables exported by a script. Create a small script that emits varlock’s resolved env in shell format: .mise/load-env.sh ```bash varlock load --format shell --compact ``` Then source it from your config: mise.toml ```toml [env] _.source = "./.mise/load-env.sh" ``` `varlock load --format shell` outputs `export KEY='value'` lines; `--compact` skips undefined optional values. This puts values in your shell Anything loaded this way lives in your shell session and is inherited by every process you launch, and varlock’s [log redaction](/guides/secrets/#redaction-in-varlock-run) does **not** apply, since it only protects the output of `varlock run`. Scope a dedicated file to just the non-secret values you want ambient (and keep secrets in [tasks](#running-your-commands-as-tasks)), or use [direnv](/integrations/direnv/) if you specifically want enter/leave shell loading. mise’s `redact` isn’t a substitute for `varlock run` mise has its own [redactions](https://mise.jdx.dev/environments/#redactions) (`redact = true`, or a `redactions` list) that hide values from `mise run` task logs. But mise can only redact a value it manages as an env var, and by its own docs, redaction “does not prevent the values from being exported to child processes.” So it’s log hygiene, not isolation: the value still flows to everything. For real secrets, prefer [`varlock run`](#running-your-commands-as-tasks), which both scopes the value to a single subprocess **and** redacts its output before mise (or anything else) ever sees it. ## Validate when entering a project [Section titled “Validate when entering a project”](#validate-when-entering-a-project) If you just want a fast failure when a project’s environment is missing or invalid, without injecting anything into your shell, use a [hook](https://mise.jdx.dev/hooks.html) instead of `[env]`: mise.toml ```toml [hooks] enter = "varlock load > /dev/null" ``` `varlock load` exits non-zero on a validation error, so you’ll see the problem as soon as you `cd` in. Run `varlock load` directly to see the full colorized error output. # Next.js > How to integrate Varlock with Next.js for secure, type-safe environment management [![](https://img.shields.io/npm/v/@varlock/nextjs-integration?label=%40varlock%2Fnextjs-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/nextjs-integration) Varlock is a big upgrade over the [default Next.js environment variable tooling](https://nextjs.org/docs/pages/guides/environment-variables), adding validation, type safety, multi-environment management, log redaction, leak detection, and more. To integrate varlock into a Next.js application, use our [`@varlock/nextjs-integration`](https://npmx.dev/package/@varlock/nextjs-integration) package. It provides a drop-in replacement for [`@next/env`](https://www.npmjs.com/package/@next/env), the internal package that handles .env loading, plus a small config plugin that injects our additional security features. ## Setup [Section titled “Setup”](#setup) Requirements * Node.js v22 or higher * Next.js v14 or higher (v15 and v16 fully supported, including Turbopack) 1. **Install varlock and the Next.js integration package** * npm ```bash npm install @varlock/nextjs-integration varlock ``` * pnpm ```bash pnpm add @varlock/nextjs-integration varlock ``` * bun ```bash bun add @varlock/nextjs-integration varlock ``` * yarn ```bash yarn add @varlock/nextjs-integration varlock ``` * vlt ```bash vlt install @varlock/nextjs-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema` file** This will guide you through setting up your `.env.schema` file, based on your existing `.env` file(s). Make sure to review it carefully. * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Override `@next/env` with our drop-in replacement** Next.js does not have APIs we can hook into, so we must override their internal .env-loading package. Overriding dependencies is a bit different for each package manager: * npm See [NPM overrides docs](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides) package.json ```diff { +"overrides": { +"next": { +"@next/env": "npm:@varlock/nextjs-integration" + } + } } ``` npm may not apply the override on a plain `npm install` npm can be inconsistent about applying `overrides` for nested scoped packages like `@next/env`, especially when an existing `package-lock.json` has it pinned to a stale location. If after installing you hit `Cannot find module '@next/env'` or don’t see the varlock banner, delete both `node_modules` and `package-lock.json` and reinstall. See [Troubleshooting](#troubleshooting). * yarn See [yarn resolutions docs](https://yarnpkg.com/configuration/manifest#resolutions) root/package.json ```diff { +"resolutions": { +"**/@next/env": "npm:@varlock/nextjs-integration" + }, } ``` **In a monorepo, this override must be done in the monorepo’s root package.json file!** * pnpm * pnpm version 10+ See [pnpm v10 overrides docs](https://pnpm.io/settings#overrides) root/pnpm-workspace.yaml ```diff packages: # <- ⚠️ this field is also required - . # set this to '.' if not in a monorepo +overrides: +"@next/env": "npm:@varlock/nextjs-integration" ``` **This must be set in `pnpm-workspace.yaml`, which lives at the root of your repo, regardless of whether you are using a monorepo or not.** * pnpm version 9 See [pnpm v9 overrides docs](https://pnpm.io/9.x/package_json#pnpmoverrides) root/package.json ```diff { +"pnpm": { +"overrides": { +"@next/env": "npm:@varlock/nextjs-integration" + } + } } ``` **In a monorepo, this override must be done in the monorepo’s root package.json file!** * bun See [pnpm v10 overrides docs](https://pnpm.io/settings#overrides) root/pnpm-workspace.yaml ```diff packages: # <- ⚠️ this field is also required - . # set this to '.' if not in a monorepo +overrides: +"@next/env": "npm:@varlock/nextjs-integration" ``` **This must be set in `pnpm-workspace.yaml`, which lives at the root of your repo, regardless of whether you are using a monorepo or not.** * pnpm version 10+ See [pnpm v9 overrides docs](https://pnpm.io/9.x/package_json#pnpmoverrides) root/package.json ```diff { +"pnpm": { +"overrides": { +"@next/env": "npm:@varlock/nextjs-integration" + } + } } ``` **In a monorepo, this override must be done in the monorepo’s root package.json file!** * pnpm version 9 See [Bun overrides docs](https://bun.com/docs/pm/overrides) package.json ```diff { +"overrides": { +"@next/env": "npm:@varlock/nextjs-integration" + } } ``` Then re-run your package manager’s install command to apply the override: * npm ```bash npm install ``` * yarn ```bash yarn install ``` * pnpm ```bash pnpm install ``` * bun ```bash bun install ``` 4. **Enable the Next.js config plugin** At this point, varlock will now load your .env files into `process.env`. But to get the full benefits of this integration, you must add `varlockNextConfigPlugin` to your `next.config.*` file. next.config.ts ```diff import type { NextConfig } from "next"; +import { varlockNextConfigPlugin } from '@varlock/nextjs-integration/plugin'; const withVarlock = varlockNextConfigPlugin(); const nextConfig: NextConfig = { // your existing config... }; -export default nextConfig; +export default withVarlock(nextConfig); ``` 5. **Verify the override is active** Start your dev server (or run a build) and look at the startup output. When the override is working, varlock reports the `.env` files it loaded with a banner: next dev ```bash - Environments: .env.schema, ✨ loaded by varlock ✨ ``` If you **don’t** see `✨ loaded by varlock ✨`, or you get `Cannot find module '@next/env'` or `__VARLOCK_ENV is not set`, the override didn’t take effect. See [Troubleshooting](#troubleshooting) below. The override only needs to apply at build/dev time On serverless platforms (e.g. Vercel, Cloudflare), the fully-resolved env is baked into your build output by the config plugin, so the production server does not re-load `.env` files. This means you only need the `@next/env` override to be active when you run `next dev` and `next build`. There is no separate runtime config to set on the platform. *** ## Monorepos [Section titled “Monorepos”](#monorepos) In a [monorepo](/guides/monorepos/), the `@next/env` override from setup step 3 must live at the **workspace root** so every Next.js app resolves the same replacement. Package-manager overrides apply to **all** Next.js packages in the workspace, so only add the override once each app you run has its own `.env.schema` and `varlockNextConfigPlugin`. *** ## Accessing environment variables [Section titled “Accessing environment variables”](#accessing-environment-variables) You can continue to use `process.env.SOMEVAR` as usual, but we recommend using Varlock’s imported `ENV` object for better type-safety and improved developer experience: example.ts ```ts import { ENV } from 'varlock/env'; console.log(process.env.SOMEVAR); // 🆗 still works console.log(ENV.SOMEVAR); // ✨ recommended ``` Caution If you are not using the `varlockNextConfigPlugin`, only `process.env` will work. ### Type-safety and IntelliSense [Section titled “Type-safety and IntelliSense”](#type-safety-and-intellisense) To enable type-safety and IntelliSense for your env vars, enable the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) in your `.env.schema`. Note that if your schema was created using `varlock init`, it will include this by default. .env.schema ```diff +# @generateTsTypes(path='env.d.ts') # --- # your config items... ``` #### Why use `ENV` instead of `process.env`? [Section titled “Why use ENV instead of process.env?”](#why-use-env-instead-of-processenv) * Non-string values (e.g., number, boolean) are properly typed and coerced * All non-sensitive items are replaced at build time (not just `NEXT_PUBLIC_`) * Better error messages for invalid or unavailable keys * Enables future DX improvements and tighter control over what is bundled *** ## Managing multiple environments [Section titled “Managing multiple environments”](#managing-multiple-environments) Varlock can load multiple *environment-specific* `.env` files (e.g., `.env.development`, `.env.preview`, `.env.production`). By default, the environment flag is determined as follows (matching Next.js): * `test` if `NODE_ENV` is `test` * `development` if running `next dev` * `production` otherwise Tip Without a custom env flag, you cannot use non-production env files (like `.env.preview`, `.env.staging`) for non-prod deployments. Instead, we recommend explicitly setting your own environment flag using the [`@currentEnv` root decorator](/reference/root-decorators/#currentenv), e.g. `APP_ENV`. See the [environments guide](/guides/environments) for more information. Loading `.env.local` in `test` environment Next.js makes [a special exception](https://nextjs.org/docs/pages/guides/environment-variables#test-environment-variables) to skip loading `.env.local` if the current environment is `test`. Varlock does not (following Vite’s lead), but you may explicitly recreate that behavior: .env.local ```env-spec # @disable=forEnv(test) ``` Precedence of `.env.local` vs `.env.[currentEnv]` Next.js swaps the order of precedence for `.env.local` vs `.env.[currentEnv]` compared to Varlock. ### Setting the environment flag [Section titled “Setting the environment flag”](#setting-the-environment-flag) When running locally, or on a platform you control, you can set the env flag explicitly as an environment variable. However on some cloud platforms, there is a lot of magic happening, and the ability to set environment variables per branch is limited. In these cases you can use functions to transform env vars injected by the platform, like a current branch name, into the value you need. #### Local/custom scripts [Section titled “Local/custom scripts”](#localcustom-scripts) You can set the env var explicitly when you run a command, but often you will set it in `package.json` scripts: package.json ```json "scripts": { "build:preview": "APP_ENV=preview next build", "start:preview": "APP_ENV=preview next start", "build:prod": "APP_ENV=production next build", "start:prod": "APP_ENV=production next start", "test": "APP_ENV=test jest" } ``` #### Vercel [Section titled “Vercel”](#vercel) You can use the injected `VERCEL_ENV` variable to match their concept of environment types: .env.schema ```env-spec # @currentEnv=$APP_ENV # --- # @type=enum(development, preview, production) VERCEL_ENV= # @type=enum(development, preview, production, test) APP_ENV=fallback($VERCEL_ENV, development) ``` For more granular environments, use the branch name in `VERCEL_GIT_COMMIT_REF` (see Cloudflare example below). #### Cloudflare Workers Build [Section titled “Cloudflare Workers Build”](#cloudflare-workers-build) Use the branch name in `WORKERS_CI_BRANCH` to determine the environment: .env.schema ```env-spec # @currentEnv=$APP_ENV # --- WORKERS_CI_BRANCH= # @type=enum(development, preview, production, test) APP_ENV=remap($WORKERS_CI_BRANCH, "main", production, /.*/, preview, development ) ``` *** ## Managing sensitive config values [Section titled “Managing sensitive config values”](#managing-sensitive-config-values) Next.js uses the `NEXT_PUBLIC_` prefix to determine which env vars are public (bundled for the browser). Varlock lets you control this using decorators. Set [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive) and mark specific items [`@sensitive`](/reference/item-decorators/#sensitive): .env.schema ```diff +# @defaultSensitive=true # --- SECRET_FOO= # sensitive by default # @sensitive=false NON_SECRET_FOO= ``` Or, if you’d like to continue using Next.js’s prefix behavior: .env.schema ```diff +# @defaultSensitive=inferFromPrefix('NEXT_PUBLIC_') # --- FOO= # sensitive NEXT_PUBLIC_FOO= # non-sensitive, due to prefix ``` Bundling behavior All non-sensitive items are bundled at build time via `ENV`, while `process.env` replacements only include `NEXT_PUBLIC_`-prefixed items. ## Extra setup for standalone mode [Section titled “Extra setup for standalone mode”](#standalone) **⚠️ This is only needed if you are using `output: standalone`** Next’s standalone build command will not copy all our `.env` files to the `.next/standalone` directory, so we must copy them manually. Add this to your build command: package.json ```json { "scripts": { "build": "next build && cp .env.* .next/standalone", } } ``` *you may need to adjust if you don’t want to copy certain .local files* Standalone builds do not copy dependency binaries, and varlock depends on the CLI to load. So wherever you are booting your standalone server, you will also need to [install the varlock binary](/getting-started/installation/) and boot your server via [`varlock run`](/reference/cli-commands/#run) ```bash varlock run -- node .next/standalone/server.js ``` *** Encrypting the env blob When deploying to serverless platforms like Vercel, Varlock injects the fully resolved env data into your server-side build output. By default, this blob is plaintext JSON, meaning anyone with access to the build artifact can read your secrets. You can encrypt it for extra protection (e.g., against sourcemap leaks). See the [encrypted deployments guide](/guides/encrypted-deployments/) for setup instructions. *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) Almost all setup issues come down to the `@next/env` override not being applied. The fixes below are ordered by the symptom you see. Work top to bottom. * ❌ `Error: Cannot find module '@next/env'` (or no `✨ loaded by varlock ✨` banner)\ 💡 The override was recorded in your lockfile but your package manager didn’t materialize it correctly. This is a known package-manager issue with `overrides`/`resolutions` for nested scoped packages. npm in particular can create a dangling symlink under `node_modules/next/node_modules/@next/env`, or skip it entirely, when installing against a stale lockfile. * Delete **both** `node_modules` **and** your lockfile, then reinstall. Deleting the lockfile alone is not enough: * npm: `rm -rf node_modules package-lock.json && npm install` * pnpm: `rm -rf node_modules pnpm-lock.yaml && pnpm install` * yarn: `rm -rf node_modules yarn.lock && yarn install` * bun: `rm -rf node_modules bun.lock bun.lockb && bun install` * Commit the regenerated lockfile. * Re-verify with the banner above, or directly: `cat node_modules/@next/env/package.json` should show `"name": "@varlock/nextjs-integration"`. * ❌ `process.env.__VARLOCK_ENV is not set`\ 💡 The override is missing or in the wrong place, so Next loaded its own `@next/env` instead of ours. * Confirm the override config is present and in the **correct file** for your package manager (see [step 3](#setup)). For pnpm it must be in `pnpm-workspace.yaml` (v10) or `package.json#pnpm.overrides` (v9), and in a monorepo it must be at the repo root. * Re-run your package manager’s install command, then re-verify the banner. * ❌ `Error [ERR_REQUIRE_ESM]: require() of ES Module ...`\ 💡 Varlock requires node v22 or higher, which has better CJS/ESM interoperability * ❌ Every page 500s in dev with a `[turbopack-node]/transforms/transforms.ts ... lint TP1006` error, but only when the project has a `middleware.ts` file (Next 15.0–15.4 + Turbopack)\ 💡 Turbopack on those versions cannot apply JS loaders (which varlock uses) to edge-context files like middleware without breaking dev page rendering. Upgrade to `next@^15.5` (where varlock scopes its loader away from edge files automatically) or Next 16 (which fixed the underlying issue), or run `next dev` without `--turbopack`. * ❌ `Property 'SOMEVAR' does not exist on type 'TypedEnvSchema'`\ 💡 If the item does exist in your schema, then the generated types are not being loaded properly by TypeScript * make sure the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) is enabled * ensure the path to the generated types file is included in your `tsconfig.json` *** ## Reference [Section titled “Reference”](#reference) * [Root decorators reference](/reference/root-decorators) * [Item decorators reference](/reference/item-decorators) * [Functions reference](/reference/functions) * [Next.js environment variable docs](https://nextjs.org/docs/pages/guides/environment-variables) # Other languages > Integrating varlock into other languages To use varlock with other languages, [install the standalone binary](/getting-started/installation/#as-a-standalone-binary) rather than a JS package manager, and run your app under [`varlock run`](/reference/cli-commands/#run). It loads and validates your environment, then runs your command with the resolved values injected into the process. ```bash varlock run -- ``` ## Generated env modules [Section titled “Generated env modules”](#generated-env-modules) Varlock can generate a typed env module from your schema for several languages via per-language [`@generate*Env`](/reference/root-decorators/#code-generation) root decorators (see the [Code generation guide](/guides/code-generation/) for the full picture). Each generated file is a small, self-contained module with no hand-rolled JSON parsing, containing: * a **typed, coerced env** object/type (numbers, booleans, parsed objects, not raw strings) * a **loader** that reads the injected [`__VARLOCK_ENV`](/reference/reserved-variables/#__varlock_env) blob and returns those typed values * a **`SENSITIVE_KEYS`** constant listing which keys hold sensitive values, so you can build your own redaction or leak-scanning It exposes only these primitives (no global singleton or imposed caching), so it stays idiomatic in each language. Pick your language for setup and usage: | Language | Decorator | Guide | | -------- | -------------------- | ------------------------------- | | Python | `@generatePythonEnv` | [Python](/integrations/python/) | | Rust | `@generateRustEnv` | [Rust](/integrations/rust/) | | Go | `@generateGoEnv` | [Go](/integrations/go/) | | PHP | `@generatePhpEnv` | [PHP](/integrations/php/) | Files are generated automatically on [`varlock load`](/reference/cli-commands/#load) and [`varlock run`](/reference/cli-commands/#run), or explicitly via [`varlock codegen`](/reference/cli-commands/#codegen). The loaders read `__VARLOCK_ENV`, so run your program under [`varlock run`](/reference/cli-commands/#run), which always injects a **plaintext** blob. The generated loaders don’t decrypt, so they don’t support [`@encryptInjectedEnv`](/reference/root-decorators/#encryptinjectedenv) (a JS/SSR build-output feature); if a loader is handed an encrypted blob it fails with a clear message rather than a raw parse error. Note that `varlock run --inject vars` strips the `__VARLOCK_ENV` blob from the child process, so the generated loaders can’t work under it. Use the default injection mode (or `--inject blob`) when your program loads a generated module. ## Languages without a generated module [Section titled “Languages without a generated module”](#languages-without-a-generated-module) For a language that doesn’t have a `@generate*Env` decorator yet, you have two options, both of which work under `varlock run`: **Read individual env vars.** Every config key is injected as a plain environment variable, so read them the way you normally would (`os.environ`, `getenv`, etc.). These values are always **strings** (uncoerced), so parse numbers/booleans yourself. **Parse the blob yourself.** For coerced values and metadata, read the [`__VARLOCK_ENV`](/reference/reserved-variables/#__varlock_env) environment variable, a JSON object with a `config` map keyed by env var name. Skip entries with no `value` (unset optionals), and use `isSensitive` to build redaction: ```jsonc // shape of __VARLOCK_ENV { "config": { "DB_PORT": { "value": 5432, "isSensitive": false }, "API_KEY": { "value": "…", "isSensitive": true } } } ``` The generated modules above do exactly this, and they’re the fastest path if your language is supported. We’re planning first-class helper libraries and more generated languages. Raw string env vars Individual config keys are also injected as plain environment variables, but those values are always strings (uncoerced). Prefer the generated loader / `__VARLOCK_ENV` when you want coerced types. Keys whose names aren’t valid identifiers (they contain `.` or `-`) are **omitted from the typed value** in Python, Rust, Go, and PHP, since they can’t be a struct field / property. Read them from the raw environment variable instead (they’re still injected, and still listed in `SENSITIVE_KEYS`). TypeScript keeps them, accessible as `ENV['MY-KEY']`. Sensitive values in the blob `__VARLOCK_ENV` includes resolved sensitive values and metadata used by varlock integrations. For interactive shells or workflows where subprocesses inspect the environment, use [`--inject vars`](/reference/cli-commands/#run) and read individual vars (as strings) instead. Note that this also disables the generated loaders, which need the blob. **We are planning deeper integrations with other languages. Want to help us build these? Join our [Discord](https://chat.dmno.dev).** # Integrations Overview > How Varlock integrates with popular frameworks, bundlers, and runtimes Varlock ships with official integrations that wire the CLI, runtime helpers, and framework-specific plugins together so your configuration is validated, type-safe, and protected everywhere it runs. If you are working in a monorepo, start with the [Monorepos guide](/guides/monorepos/) for schema layout, root-level dependency overrides, and CI patterns before picking per-framework integrations below. Integrations let you: * load and validate `.env` files automatically during dev and use them in CI/CD and production * inject config into your code at build or request time * enable runtime protections such as leak prevention, and log redaction ## Official integrations [Section titled “Official integrations”](#official-integrations) * [JavaScript / Node.js](/integrations/javascript/): use `varlock` in custom toolchains, scripts, and servers * [Bun](/integrations/bun/): instructions to set up Varlock with Bun * [Next.js](/integrations/nextjs/): drop-in replacement for `@next/env` * [Vite](/integrations/vite/): Vite plugin that validates and replaces at build time * [Qwik](/integrations/vite/): use the Vite integration * [React Router](/integrations/vite/): use the Vite integration * [Cloudflare Workers](/integrations/cloudflare/): use the Vite integration or Wrangler vars/secrets directly * [Astro](/integrations/astro/): Astro integration built on top of our Vite plugin * [SvelteKit](/integrations/sveltekit/): use the Vite integration, or the Cloudflare integration’s SvelteKit plugin when deploying to Workers * [TanStack Start](/integrations/tanstack-start/): use the Vite integration, or the Cloudflare integration when deploying to Workers * [Expo / React Native](/integrations/expo/): Expo and React Native CLI: Babel plugin + Metro config wrapper with compile-time replacements and server route support (Expo only) * [GitHub Actions](/integrations/github-action/): validate your `.env.schema` in GitHub Actions workflows * [Python](/integrations/python/): `varlock run` + a generated typed env module (also Pydantic Settings / environs) * [Rust](/integrations/rust/): a generated, serde-derived env module * [Go](/integrations/go/): a generated env package * [PHP](/integrations/php/): a generated, typed env class * [Other languages](/integrations/other-languages/): generated-module overview + guidance for any other non-JS runtime * [Docker](/guides/docker/): a Docker wrapper around the varlock CLI including examples * [mise](/integrations/mise/): install varlock and wire validated env vars into your tasks * [direnv](/integrations/direnv/): load validated env vars directly into your shell session ## Coming soon [Section titled “Coming soon”](#coming-soon) We’re working on more first-party integrations for popular runtimes, frameworks and hosting platforms. If yours isn’t listed yet, let us know! Request an integration Join us on [Discord](https://chat.dmno.dev) or open a GitHub issue describing your use case so we can prioritize it. # PHP > Use varlock with PHP via a generated, typed env class To use varlock with PHP, [install the standalone binary](/getting-started/installation/#as-a-standalone-binary) and run your app under [`varlock run`](/reference/cli-commands/#run). It loads and validates your env, then injects it into the process. Add [`@generatePhpEnv`](/reference/root-decorators/#generatephpenv) to your schema to generate a small, self-contained class: a readonly `Env` with typed promoted properties, a static `load()` that parses the injected [`__VARLOCK_ENV`](/reference/reserved-variables/#__varlock_env) blob, and a `SENSITIVE_KEYS` constant. .env.schema ```env-spec # @generatePhpEnv(path=Env.php) ``` The file is regenerated automatically on [`varlock load`](/reference/cli-commands/#load) and [`varlock run`](/reference/cli-commands/#run), or explicitly with [`varlock codegen`](/reference/cli-commands/#codegen). Requires **PHP 8.1+** (readonly promoted properties). ## Reading values [Section titled “Reading values”](#reading-values) Call `load()` once, then read fields off the object. Required fields are the plain type, optional ones are nullable (`?type`): ```php DB_PORT; // ?int $debug = $env->DEBUG; // ?bool // SENSITIVE_KEYS is available for your own redaction foreach (Env::SENSITIVE_KEYS as $key) { /* ... */ } ``` ```bash varlock run -- php main.php ``` `load()` throws a clear error if `__VARLOCK_ENV` is missing (e.g. you forgot `varlock run`) or if a required key is absent. ## Sharing one instance [Section titled “Sharing one instance”](#sharing-one-instance) Every `load()` call re-parses the blob. To avoid reloading, bind the loaded instance wherever your app keeps shared services, e.g. your DI container: bootstrap.php ```php $container->instance(Env::class, Env::load()); // resolve Env anywhere, type-hinted ``` No container? A tiny static holder works: ```php final class Config { public static Env $env; } Config::$env = Env::load(); // elsewhere: Config::$env->DB_PORT (?int) ``` ## Composer / PSR-4 [Section titled “Composer / PSR-4”](#composer--psr-4) By default the class is a global `final class Env` loaded via `require_once`. For Composer projects, set `namespace=` and/or `class=` on the decorator and register the file via classmap autoload instead: .env.schema ```env-spec # @generatePhpEnv(path=src/Env.php, namespace="App\Config", class=AppEnv) ``` composer.json ```json { "autoload": { "classmap": ["src/Env.php"] } } ``` Under PHP-FPM, make sure the pool passes `__VARLOCK_ENV` through (`clear_env = no`). Note Every key is also injected as a plain environment variable, so you can read any of them with `getenv()` (raw strings) if you don’t need the typed module. Keys whose names aren’t valid identifiers (they contain `.` or `-`) can’t be properties, so read those from the env var directly. See [Other languages](/integrations/other-languages/) for the shared details on the injected blob, raw env vars, and encryption. # Python > Use varlock with Python via a generated typed env module, Pydantic Settings, or environs To use varlock with Python, [install the standalone binary](/getting-started/installation/#as-a-standalone-binary) and run your app under [`varlock run`](/reference/cli-commands/#run). It loads and validates your env, then injects it into the process. Add [`@generatePythonEnv`](/reference/root-decorators/#generatepythonenv) to your schema to generate a small, self-contained module: a coerced `Env` [`TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict), a `load_env()` function that parses the injected [`__VARLOCK_ENV`](/reference/reserved-variables/#__varlock_env) blob, and a `SENSITIVE_KEYS` constant. .env.schema ```env-spec # @generatePythonEnv(path=env.py) ``` The file is regenerated automatically on [`varlock load`](/reference/cli-commands/#load) and [`varlock run`](/reference/cli-commands/#run), or explicitly with [`varlock codegen`](/reference/cli-commands/#codegen). It has no dependencies, importing it has no side effects, and it imports on any **Python 3.7+** (it uses `from __future__ import annotations`, so `NotRequired`/`Literal` are type-checker-only). ## Reading values [Section titled “Reading values”](#reading-values) Call `load_env()` once, then read keys off the dict. Values are coerced (`int`/`bool`/etc.), not raw strings: ```python from env import load_env, SENSITIVE_KEYS env = load_env() # call once, hold or pass it around port = env["DB_PORT"] # int debug = env["DEBUG"] # bool # build your own redaction using SENSITIVE_KEYS safe = {k: ("***" if k in SENSITIVE_KEYS else v) for k, v in env.items()} ``` ```bash varlock run -- python main.py ``` `load_env()` raises a clear error if `__VARLOCK_ENV` is missing (e.g. you forgot `varlock run`). Optional keys that are unset are absent from the dict (`NotRequired`), not present as `None`. ## Sharing one instance [Section titled “Sharing one instance”](#sharing-one-instance) Every `load_env()` call re-parses the blob. To avoid reloading, load once in a config module and share it, the idiomatic “settings module” pattern: config.py ```python from env import load_env env = load_env() # loaded once, typed as Env ``` anywhere.py ```python from config import env port = env["DB_PORT"] # int ``` ## Running your server [Section titled “Running your server”](#running-your-server) [`varlock run`](/reference/cli-commands/#run) wraps any command, so boot your ASGI server, task runner, or scripts through it. Local dev, CI, and production all use the same schema: ```bash varlock run -- uvicorn main:app --reload varlock run -- poetry run pytest varlock run -- python manage.py runserver ``` Shell expansion Environment variables in the command itself are expanded by your shell **before** varlock runs. Pass overrides on the left (`APP_ENV=production varlock run -- …`) or use [`varlock printenv`](/reference/cli-commands/#printenv) to read resolved values into the shell. ## Using a settings library [Section titled “Using a settings library”](#using-a-settings-library) If you’d rather keep the settings library you already use, every key is also injected as a plain environment variable, so [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/), [environs](https://github.com/sloria/environs), and friends work unchanged. They read the **raw string** values (e.g. `"5432"`, `"true"`), so your settings class is what coerces them: settings.py ```python from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(extra="ignore") app_env: str database_url: str openai_api_key: str settings = Settings() ``` config.py ```python from environs import Env env = Env() # Do not call env.read_env(); varlock run already populated os.environ DATABASE_URL = env.str("DATABASE_URL") DEBUG = env.bool("DEBUG", default=False) ``` Skip `.env` file loading in Python When using varlock, don’t also load `.env` from Python (`python-dotenv`, Pydantic’s `env_file=`, etc.) unless you intentionally want a second source of truth. Varlock is the single resolver: your schema, plugins, and environment files all flow through it before Python starts. Note Every key is also injected as a plain environment variable, so you can read any of them with `os.environ` (raw strings) if you don’t need the typed module. Keys whose names aren’t valid identifiers (they contain `.` or `-`) can’t be `TypedDict` fields, so read those from the env var directly. See [Other languages](/integrations/other-languages/) for the shared details on the injected blob, raw env vars, and encryption. # Rust > Use varlock with Rust via a generated, serde-derived env module To use varlock with Rust, [install the standalone binary](/getting-started/installation/#as-a-standalone-binary) and run your app under [`varlock run`](/reference/cli-commands/#run). It loads and validates your env, then injects it into the process. Add [`@generateRustEnv`](/reference/root-decorators/#generaterustenv) to your schema to generate a small, self-contained module: a serde-derived `Env` struct, a `load()` function that parses the injected [`__VARLOCK_ENV`](/reference/reserved-variables/#__varlock_env) blob, and a `SENSITIVE_KEYS` constant. .env.schema ```env-spec # @generateRustEnv(path=src/env.rs) ``` The file is regenerated automatically on [`varlock load`](/reference/cli-commands/#load) and [`varlock run`](/reference/cli-commands/#run), or explicitly with [`varlock codegen`](/reference/cli-commands/#codegen). It needs `serde` and `serde_json`: ```bash cargo add serde --features derive cargo add serde_json ``` ## Reading values [Section titled “Reading values”](#reading-values) Call `load()` once, then read fields off the struct. Required fields are the plain type, optional ones are `Option`: ```rust mod env; fn main() -> Result<(), Box> { let e = env::load()?; // call once, hold or pass `e` around let host = e.db_host; // String (required field) if let Some(port) = e.db_port { // Option (optional field) println!("{host}:{port}"); } Ok(()) } ``` ```bash varlock run -- cargo run ``` `load()` returns a clear error if `__VARLOCK_ENV` is missing (e.g. you forgot `varlock run`) or if a required key is absent. The struct derives a **redacting `Debug`**: sensitive fields print as ``, so `{:?}` won’t leak secrets. ## Sharing one instance [Section titled “Sharing one instance”](#sharing-one-instance) Every `load()` call re-parses the blob. If you want a process-wide instance, opt into a `LazyLock` yourself (loaded once, on first access; `LazyLock` needs Rust 1.80+, or use `once_cell::sync::Lazy` on older toolchains): config.rs ```rust use std::sync::LazyLock; use crate::env; pub static ENV: LazyLock = LazyLock::new(|| env::load().expect("failed to load env")); ``` anywhere.rs ```rust use crate::config::ENV; let port = ENV.db_port; // Option ``` Prefer no globals? Call `env::load()?` once in `main` and pass `&Env` through your app state. The type comes along either way. Note Every key is also injected as a plain environment variable, so you can read any of them with `std::env::var` (raw strings) if you don’t need the typed module. Enums become plain `String` (Rust has no string-literal type), and keys whose names aren’t valid identifiers (they contain `.` or `-`) can’t be struct fields, so read those from the env var directly. See [Other languages](/integrations/other-languages/) for the shared details on the injected blob, raw env vars, and encryption. # SvelteKit > How to integrate varlock with SvelteKit for secure, type-safe environment management [SvelteKit](https://svelte.dev/docs/kit) is built on [Vite](https://vite.dev), so there’s no dedicated SvelteKit package. You use the [Vite integration](/integrations/vite/) the same way regardless of where you deploy. Add `varlockVitePlugin()` to your Vite config ([setup below](#setup)) and it works across deploy targets. If you deploy to **Cloudflare Workers** (via [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare)), the plugin detects the adapter and automatically wires up the runtime env-loader. You just need `@varlock/cloudflare-integration` installed as well (it ships `varlock-wrangler`). See [Cloudflare Workers setup](#setup-for-cloudflare-workers). Check out the [SvelteKit example project](https://github.com/dmno-dev/varlock-examples/tree/main/examples/sveltekit) for a working reference. *** ## Setup [Section titled “Setup”](#setup) [![](https://img.shields.io/npm/v/@varlock/vite-integration?label=%40varlock%2Fvite-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/vite-integration) Use `varlockVitePlugin` exactly as you would in any Vite project. This is the setup for every deploy target. See [Cloudflare Workers](#setup-for-cloudflare-workers) below for the one extra package CF deployments need. 1. **Install packages** * npm ```bash npm install @varlock/vite-integration varlock ``` * pnpm ```bash pnpm add @varlock/vite-integration varlock ``` * bun ```bash bun add @varlock/vite-integration varlock ``` * yarn ```bash yarn add @varlock/vite-integration varlock ``` * vlt ```bash vlt install @varlock/vite-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema`** * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Add the plugin to your Vite config.** It should come **before** `sveltekit()`: vite.config.ts ```diff import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; +import { varlockVitePlugin } from '@varlock/vite-integration'; export default defineConfig({ plugins: [ +varlockVitePlugin(), sveltekit(), ], }); ``` Next steps See the [**Vite integration page**](/integrations/vite/) for configuration options, SSR injection modes, accessing env vars, managing environments, sensitive values, and more. *** ## Setup for Cloudflare Workers [Section titled “Setup for Cloudflare Workers”](#setup-for-cloudflare-workers) [![](https://img.shields.io/npm/v/@varlock/cloudflare-integration?label=%40varlock%2Fcloudflare-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/cloudflare-integration) For SvelteKit projects deploying to Cloudflare Workers via [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare), use the same `varlockVitePlugin()` from the [setup above](#setup). When it detects the Cloudflare adapter, it automatically injects the runtime env-loader into SvelteKit’s SSR entry so the resolved env is available inside the worker. You just need to additionally install `@varlock/cloudflare-integration`, which provides `varlock-wrangler` and the Cloudflare-specific loader the plugin pulls in. (The standard `varlockCloudflareVitePlugin` isn’t used here because `@cloudflare/vite-plugin` doesn’t currently support SvelteKit. See [cloudflare/workers-sdk#8922](https://github.com/cloudflare/workers-sdk/issues/8922).) The adapter is detected whether you configure it in `svelte.config.js` or inline in your Vite config (SvelteKit ≥ 2.62), so no extra setup is needed either way. Static-only SvelteKit sites If your SvelteKit app is fully static (no SSR, typically using `@sveltejs/adapter-static` and deployed to Cloudflare Pages or any CDN), you don’t need `@varlock/cloudflare-integration` at all. The [base setup](#setup) is enough, since there’s no server bundle for us to inject into. ### Setup [Section titled “Setup”](#setup-1) 1. **Install packages** * npm ```bash npm install @varlock/vite-integration @varlock/cloudflare-integration varlock ``` * pnpm ```bash pnpm add @varlock/vite-integration @varlock/cloudflare-integration varlock ``` * bun ```bash bun add @varlock/vite-integration @varlock/cloudflare-integration varlock ``` * yarn ```bash yarn add @varlock/vite-integration @varlock/cloudflare-integration varlock ``` * vlt ```bash vlt install @varlock/vite-integration @varlock/cloudflare-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema`** * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Add the plugin to your Vite config.** This is the same as the base setup; it auto-detects the Cloudflare adapter: vite.config.ts ```diff import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; +import { varlockVitePlugin } from '@varlock/vite-integration'; export default defineConfig({ plugins: [ +varlockVitePlugin(), sveltekit(), ], }); ``` 4. **Deploy with `varlock-wrangler`** Use `varlock-wrangler deploy` instead of `wrangler deploy` in your deploy script: package.json ```json { "scripts": { "dev": "vite dev", "build": "vite build", "deploy": "npm run build && varlock-wrangler deploy" } } ``` If you deploy via [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) instead of a local/CI script, override the deploy command in your Cloudflare dashboard under **Settings → Build → Deploy command**. See [Workers Builds](#deploying-via-cloudflare-workers-builds) below. ### How it works [Section titled “How it works”](#how-it-works) * **In dev**: `vite dev` runs SvelteKit’s dev server; varlock resolves env via its normal flow and makes it available on `ENV.*`. * **In production**: The SvelteKit SSR bundle has a `cloudflare:workers` runtime loader injected at the top of its server entry. At worker boot, it reads the `__VARLOCK_ENV` binding and hydrates varlock’s runtime. The loader is guarded by a `navigator.userAgent === 'Cloudflare-Workers'` check so SvelteKit’s Node-side postbuild steps (prerender, fallback) don’t try to resolve `cloudflare:workers`. * `varlock-wrangler deploy` uploads non-sensitive values as Cloudflare vars and sensitive values as secrets. ### Deploying via Cloudflare Workers Builds [Section titled “Deploying via Cloudflare Workers Builds”](#deploying-via-cloudflare-workers-builds) If you deploy through [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), two pieces of configuration must be set in the dashboard, and neither can be committed to the repo: * **Override the Deploy command.** Under **Settings → Build → Deploy command**, replace the default `npx wrangler deploy` with `npx varlock-wrangler deploy`. Without this, Cloudflare runs stock `wrangler deploy`, which skips varlock resolution and leaves your worker without its resolved vars/secrets. * **Set any *secret-zero* vars under Build variables.** Any env vars varlock itself needs *during load* (e.g. a 1Password service account token, a GCP key) must be set under **Settings → Build → Variables and Secrets** so they’re available at build time. `varlock-wrangler deploy` then resolves your full env graph and uploads the result to the worker runtime as regular vars/secrets. Next steps See the [**Cloudflare integration page**](/integrations/cloudflare/) for more on `varlock-wrangler`, multi-environment setup, and [Workers Builds configuration](/integrations/cloudflare/#cloudflare-workers-builds-cicd). *** ## Migrating from SvelteKit’s `$env/*` [Section titled “Migrating from SvelteKit’s $env/\*”](#migrating-from-sveltekits-env) SvelteKit ships [four built-in env modules](https://svelte.dev/docs/kit/$env-static-private) (`$env/static/private`, `$env/static/public`, `$env/dynamic/private`, `$env/dynamic/public`) which split env vars along two axes: *static vs dynamic* (inlined at build vs looked up at runtime) and *private vs public* (server-only vs bundled for the browser, gated by the `PUBLIC_` prefix). The key conceptual shift is that SvelteKit’s system is **access-driven**: the same underlying variable behaves differently depending on which module you import it from, and public/private is inferred from the variable’s name. Varlock is **schema-driven**: each item is declared once in `.env.schema` with decorators that determine its behavior (sensitivity, type, validation, whether it’s inlined or resolved at runtime), and every access site uses the same `ENV` object. You describe the variable once, and that description is authoritative everywhere it’s used. Varlock replaces all four `$env/*` modules with a single `ENV` object from `varlock/env`. The same two axes still exist, but they’re controlled by the schema rather than the import path: | SvelteKit concept | Varlock equivalent | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$env/static/public` | `ENV.FOO` where `@sensitive=false`; non-sensitive values are inlined at build time on both client and server | | `$env/static/private` | `ENV.FOO` where `@sensitive=true`, referenced from SSR/server code; inlined into the server bundle, build errors if referenced from client code | | `$env/dynamic/public` | **Not yet supported.** Non-sensitive values are currently always inlined at build time (equivalent to `$env/static/public`). Runtime-resolved public values are on the roadmap. | | `$env/dynamic/private` | `ENV.FOO`; in server contexts it’s read from the live runtime env (e.g. Cloudflare bindings via `varlockCloudflareVitePlugin`, or Node’s `process.env` elsewhere) rather than baked into the bundle | | `PUBLIC_` prefix requirement | Per-item [`@sensitive`](/reference/item-decorators/#sensitive) decorator, or a schema-wide [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive) rule | ### Migrating imports [Section titled “Migrating imports”](#migrating-imports) Everywhere you currently import from `$env/*`, switch to `varlock/env`: src/routes/+page.server.ts ```diff -import { API_KEY } from '$env/static/private'; -import { PUBLIC_API_URL } from '$env/static/public'; +import { ENV } from 'varlock/env'; export const load = async () => { const res = await fetch(`${PUBLIC_API_URL}/data`, { headers: { Authorization: `Bearer ${API_KEY}` }, const res = await fetch(`${ENV.PUBLIC_API_URL}/data`, { headers: { Authorization: `Bearer ${ENV.API_KEY}` }, }); }; ``` `ENV` works from both `+page.svelte` (client) and `+page.server.ts`/`+server.ts` (server); there’s no need to import from different paths depending on context. Varlock enforces the public/private boundary with both **build-time** checks (sensitive values are never included in the client bundle, and referencing one from client-reachable code surfaces as a build error) and **runtime** checks (log redaction and leak detection in server responses) so a mistake in either layer is caught rather than silently exposing a secret. ### Managing sensitive values [Section titled “Managing sensitive values”](#managing-sensitive-values) SvelteKit uses the `PUBLIC_` prefix to determine which vars are safe to bundle for the browser. Varlock uses decorators, either per-item or schema-wide via [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive). The simplest setup is to mark items explicitly: .env.schema ```diff +# @defaultSensitive=true # --- API_KEY= # sensitive by default # @sensitive=false PUBLIC_API_URL= # explicitly non-sensitive (bundled for browser) ``` If you’d rather keep SvelteKit’s `PUBLIC_`-prefix convention, you can infer sensitivity from the name: .env.schema ```diff +# @defaultSensitive=inferFromPrefix('PUBLIC_') # --- API_KEY= # sensitive (no prefix) PUBLIC_API_URL= # non-sensitive (has PUBLIC_ prefix) ``` Bundling behavior All non-sensitive items are replaced at build time wherever `ENV.FOO` appears. Sensitive items are only available in server contexts: build-time checks block them from client bundles, and runtime log redaction / response leak detection catch any that slip past the build layer. Encrypting the env blob When deploying SSR SvelteKit apps to serverless platforms, varlock injects the resolved env into your server-side build output as plaintext JSON. This is generally safe since it only appears in server code, but you can encrypt it for extra protection (e.g., against sourcemap leaks). See the [encrypted deployments guide](/guides/encrypted-deployments/) for setup instructions. # TanStack Start > How to integrate varlock with TanStack Start for secure, type-safe environment management [TanStack Start](https://tanstack.com/start) is a full-stack React framework built on [Vite](https://vite.dev), so there’s no dedicated TanStack Start package. Which integration you use depends on where you deploy. * **Node / Vercel / Netlify / self-hosted**: use the [**Vite integration**](/integrations/vite/). See [setup below](#setup). * **Cloudflare Workers**: use the [**Cloudflare integration**](/integrations/cloudflare/). See [Cloudflare setup below](#deploying-to-cloudflare-workers). Check out the [TanStack Start example project](https://github.com/dmno-dev/varlock-examples/tree/main/examples/tanstack-start) for a working reference. *** ## Setup (vite integration) [Section titled “Setup (vite integration)”](#setup-vite-integration) [![](https://img.shields.io/npm/v/@varlock/vite-integration?label=%40varlock%2Fvite-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/vite-integration) For any deployment target *other than* Cloudflare Workers, use `varlockVitePlugin` from the [Vite integration](/integrations/vite/). 1. **Install packages** * npm ```bash npm install @varlock/vite-integration varlock ``` * pnpm ```bash pnpm add @varlock/vite-integration varlock ``` * bun ```bash bun add @varlock/vite-integration varlock ``` * yarn ```bash yarn add @varlock/vite-integration varlock ``` * vlt ```bash vlt install @varlock/vite-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema`** This will guide you through setting up your `.env.schema` file, based on your existing `.env` file(s). Make sure to review it carefully. * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Add the plugin to your Vite config** app.config.ts ```diff import { defineConfig } from '@tanstack/react-start/config'; import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { varlockVitePlugin } from '@varlock/vite-integration'; export default defineConfig({ vite: { plugins: [ +varlockVitePlugin(), tanstackStart(), // ... other plugins ], }, }); ``` Next steps See the [**Vite integration page**](/integrations/vite/) for configuration options, SSR injection modes, accessing env vars, managing environments, sensitive values, and more. *** ## Setup for Cloudflare Workers [Section titled “Setup for Cloudflare Workers”](#setup-for-cloudflare-workers) [![](https://img.shields.io/npm/v/@varlock/cloudflare-integration?label=%40varlock%2Fcloudflare-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/cloudflare-integration) If you’re deploying your TanStack Start app to Cloudflare Workers, use `varlockCloudflareVitePlugin` from the [Cloudflare integration](/integrations/cloudflare/) instead. 1. **Install packages** You must also install `@cloudflare/vite-plugin` as a peer dependency. * npm ```bash npm install @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * pnpm ```bash pnpm add @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * bun ```bash bun add @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * yarn ```bash yarn add @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` * vlt ```bash vlt install @varlock/cloudflare-integration @cloudflare/vite-plugin varlock ``` 2. **Run `varlock init` to set up your `.env.schema`** * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Add the Cloudflare Vite plugin** app.config.ts ```diff import { defineConfig } from '@tanstack/react-start/config'; import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { varlockCloudflareVitePlugin } from '@varlock/cloudflare-integration'; export default defineConfig({ vite: { plugins: [ +varlockCloudflareVitePlugin(), tanstackStart(), // ... ], }, }); ``` 4. **Deploy with `varlock-wrangler`** Use `varlock-wrangler deploy` instead of `wrangler deploy` in your deploy script: package.json ```json { "scripts": { "deploy": "varlock-wrangler deploy" } } ``` If you deploy via [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) instead of a local/CI script, override the deploy command in your Cloudflare dashboard under **Settings → Build → Deploy command**. See [Workers Builds](#deploying-via-cloudflare-workers-builds) below. Next steps See the [**Cloudflare integration page**](/integrations/cloudflare/) for more on `varlock-wrangler`, multi-environment setup, and deploying secrets. ### Deploying via Cloudflare Workers Builds [Section titled “Deploying via Cloudflare Workers Builds”](#deploying-via-cloudflare-workers-builds) If you deploy through [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), two pieces of configuration must be set in the dashboard, and neither can be committed to the repo: * **Override the Deploy command.** Under **Settings → Build → Deploy command**, replace the default `npx wrangler deploy` with `npx varlock-wrangler deploy`. Without this, Cloudflare runs stock `wrangler deploy`, which skips varlock resolution and leaves your worker without its resolved vars/secrets. * **Set any *secret-zero* vars under Build variables.** Any env vars varlock itself needs *during load* (e.g. a 1Password service account token, a GCP key) must be set under **Settings → Build → Variables and Secrets** so they’re available at build time. `varlock-wrangler deploy` then resolves your full env graph and uploads the result to the worker runtime as regular vars/secrets. See the [Cloudflare integration page](/integrations/cloudflare/) for more detail on `varlock-wrangler` and [Workers Builds configuration](/integrations/cloudflare/#cloudflare-workers-builds-cicd). *** ## Compared to TanStack Start’s built-in env var handling [Section titled “Compared to TanStack Start’s built-in env var handling”](#compared-to-tanstack-starts-built-in-env-var-handling) TanStack Start inherits [Vite’s env var approach](https://tanstack.com/start/v0/docs/framework/react/guide/environment-variables): `process.env` on the server and `import.meta.env` on the client, with only `VITE_`-prefixed vars exposed client-side. Their docs recommend maintaining a manual `src/env.d.ts` for TypeScript support and separate Zod schemas for runtime validation. With varlock, your `.env.schema` is the single source of truth. It handles validation, type coercion, TypeScript generation, sensitivity controls, and more, replacing the need for separate type declarations, Zod schemas, and the `VITE_` prefix convention. You also get leak detection (sensitive values are automatically scrubbed from outgoing HTTP responses) and clear error messages when env vars are missing or invalid, so no more silent `undefined` values at runtime. See the [Vite integration page](/integrations/vite/) for the full details. Encrypting the env blob When deploying SSR TanStack Start apps to serverless platforms, varlock injects the resolved env into your server-side build output as plaintext JSON. This is generally safe since it only appears in server code, but you can encrypt it for extra protection (e.g., against sourcemap leaks). See the [encrypted deployments guide](/guides/encrypted-deployments/) for setup instructions. # Vite > How to integrate varlock with Vite for secure, type-safe environment management [![](https://img.shields.io/npm/v/@varlock/vite-integration?label=%40varlock%2Fvite-integration\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/vite-integration) Some frameworks use Vite under the hood, and some projects use Vite directly. Either way, often there is some [automatic loading of .env files](https://vite.dev/guide/env-and-mode.html) happening, but it is fairly limited. To integrate varlock into a Vite-powered application, you must use our [`@varlock/vite-integration`](https://npmx.dev/package/@varlock/vite-integration) package, which is a [Vite plugin](https://vite.dev/guide/using-plugins.html). This plugin does a few things: * Loading and validating your .env files using varlock, injecting resolved env into process.env at build/dev time * Simplifies using env vars within your `vite.config.*` file * Build time replacements of `ENV.xxx` of non-sensitive items (no prefix required) * Within SSR contexts, injecting additional initialization code and enabling additional [security features](https://varlock.dev/guides/secrets/#security-enhancements) Astro users For [Astro](https://astro.build) - which is also powered by Vite - you should use our [Astro integration](/integrations/astro/). SvelteKit users For [SvelteKit](https://svelte.dev/docs/kit), the plain Vite integration below works for most adapters. If you’re deploying to Cloudflare Workers via `@sveltejs/adapter-cloudflare`, see our [SvelteKit guide](/integrations/sveltekit/), which points you at the Cloudflare-specific plugin. Deploying a non-static site to Cloudflare Workers? If your project has an SSR/server component that runs on Cloudflare Workers, the plain Vite integration won’t wire up runtime secrets correctly: secrets would end up bundled into the worker instead of being stored as Cloudflare secrets. Use the [Cloudflare integration](/integrations/cloudflare/) (or the [SvelteKit guide](/integrations/sveltekit/) for SvelteKit) instead. This page is fine for **static-only** Vite sites. ## Frameworks that use Vite [Section titled “Frameworks that use Vite”](#frameworks) If you’re using [Qwik](https://qwik.dev/) or [React Router](https://reactrouter.com/)/[Remix](https://remix.run/), you can follow the Vite instructions below. If you’re using [Cloudflare Workers](https://developers.cloudflare.com/workers/) with the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/), use the [Cloudflare integration](/integrations/cloudflare/) instead of this page. For anything framework-specific on Cloudflare Workers (like Next.js) follow the [integration](/integrations/overview) docs for that framework. ## Setup [Section titled “Setup”](#setup) Requirements * Node.js v22 or higher * Vite v5 or higher 1. **Install varlock and the Vite integration package** * npm ```bash npm install @varlock/vite-integration varlock ``` * pnpm ```bash pnpm add @varlock/vite-integration varlock ``` * bun ```bash bun add @varlock/vite-integration varlock ``` * yarn ```bash yarn add @varlock/vite-integration varlock ``` * vlt ```bash vlt install @varlock/vite-integration varlock ``` 2. **Run `varlock init` to set up your `.env.schema` file** This will guide you through setting up your `.env.schema` file, based on your existing `.env` file(s). Make sure to review it carefully. * npm ```bash npm exec -- varlock init ``` * pnpm ```bash pnpm exec -- varlock init ``` * bun ```bash bunx varlock init ``` * vlt ```bash vlx -- varlock init ``` * yarn ```bash yarn exec -- varlock init ``` 3. **Enable the Vite config plugin** You must add our `varlockVitePlugin` to your `vite.config.*` file: vite.config.ts ```diff import { defineConfig } from 'vite'; +import { varlockVitePlugin } from '@varlock/vite-integration'; export default defineConfig({ plugins: [varlockVitePlugin(), otherPlugin()] }); ``` *** ## Custom .env file location [Section titled “Custom .env file location”](#custom-env-file-location) By default, varlock loads `.env` files from the current working directory. If you store your `.env` files in a custom directory, you can configure this using the `varlock.loadPath` option in your `package.json`: package.json ```json { "varlock": { "loadPath": "./envs/" } } ``` This works for all varlock commands and integrations, not just Vite. The `loadPath` can point to a directory (trailing `/` recommended) or a specific `.env` file. The varlock CLI `--path` flag overrides this setting when provided. Varlock looks for this config in the `package.json` in the current working directory only; it does not walk up to parent directories. Directory vs file path Point `loadPath` to a **directory** if you want varlock to automatically load all relevant files (`.env.schema`, `.env`, `.env.local`, etc.). Pointing to a specific file will only load that file and anything it explicitly imports via `@import`. ### Loading from multiple directories [Section titled “Loading from multiple directories”](#loading-from-multiple-directories) You can also provide an array of paths to combine env vars from multiple locations, useful in monorepos where different packages each have their own `.env` files: package.json ```json { "varlock": { "loadPath": ["./apps/my-package/envs/", "./apps/other-package/envs/"] } } ``` Each path in the array is loaded independently. Paths listed **later** in the array take higher precedence when the same variable is defined in multiple locations. Vite’s `envDir` option is not supported If you are using Vite’s `envDir` option to load `.env` files from a custom directory, note that **varlock ignores this option**. Use `varlock.loadPath` in your `package.json` instead, as shown above. Varlock will show a warning if it detects `envDir` is set. *** ## SSR Code Injection [Section titled “SSR Code Injection”](#ssr-code-injection) Within SSR builds, this plugin will automatically inject varlock initialization code into your entry points. There are 3 modes to choose from and specify during plugin initialization. For example: ```ts varlockVitePlugin({ ssrInjectMode: 'auto-load' }) ``` * `init-only` - injects varlock initialization code, but does not load the env vars. You must still boot your app via `varlock run` in this mode. * `auto-load` - injects `import 'varlock/auto-load';` to load your resolved env via the varlock CLI * `resolved-env` - injects the fully resolved env data into your built code. This is useful in environments like Vercel/Cloudflare/etc where you have no control over your build command, and limited access to use CLI commands or the filesystem. See the [encrypting the env blob](#encrypting-the-env-blob) section for more information. **If not specified, we will attempt to infer the correct mode based on the presence of other vite plugins and environment variables, which give us hints about how your application will be run.** Otherwise defaulting to `init-only`. ## Accessing environment variables [Section titled “Accessing environment variables”](#accessing-environment-variables) You can continue to use `import.meta.env.SOMEVAR` as usual, but we recommend using varlock’s imported `ENV` object for better type-safety and improved developer experience: example.ts ```ts import { ENV } from 'varlock/env'; console.log(import.meta.env.SOMEVAR); // 🆗 still works console.log(ENV.SOMEVAR); // ✨ recommended ``` #### Why use `ENV` instead of `import.meta.env`? [Section titled “Why use ENV instead of import.meta.env?”](#why-use-env-instead-of-importmetaenv) * Non-string values (e.g., number, boolean) are properly typed and coerced * All non-sensitive items are replaced at build time (not just `VITE_` prefixed ones) * Better error messages for invalid or unavailable keys * Enables future DX improvements and tighter control over what is bundled ### Within `vite.config.*` [Section titled “Within vite.config.\*”](#within-viteconfig) It’s often useful to be able to access env vars in your Vite config. Without varlock, it’s a bit awkward, but with varlock it’s already available. Import varlock’s `ENV` object and reference env vars via `ENV.SOME_ITEM` like you do everywhere else. vite.config.ts ```diff import { defineConfig } from 'vite'; import { varlockVitePlugin } from '@varlock/vite-integration'; +import { ENV } from 'varlock/env'; +doSomethingWithEnvVar(ENV.FOO); export default defineConfig({ /* ... */ }); ``` TypeScript config If you find you are not getting type completion on `ENV`, you may need to add your vite config and generated type files (usually `env.d.ts`) to your `tsconfig.json`’s `include` array. ### Within HTML templates [Section titled “Within HTML templates”](#within-html-templates) Vite [natively supports](https://vite.dev/guide/env-and-mode.html#html-constant-replacement) injecting env vars into HTML files using a special syntax like `%SOME_VAR%`. This plugin injects additional replacements for strings like `%ENV.SOME_VAR%`. Note that unlike the native functionality which does not replace missing/non-existant items, we will try to replace all items, and will throw helpful errors if something goes wrong. HTML comments Note that replacements anywhere in the file, including HTML comments, are still attempted and can cause errors. For example `` will still fail! ### Within other scripts [Section titled “Within other scripts”](#within-other-scripts) Even in a static front-end project, you may have other scripts in your project that rely on sensitive config. You can use [`varlock run`](/reference/cli-commands/#run) to inject resolved config into other scripts as regular environment vars. * npm ```bash npm exec -- varlock run -- node ./script.js ``` * pnpm ```bash pnpm exec -- varlock run -- node ./script.js ``` * bun ```bash bunx varlock run -- node ./script.js ``` * vlt ```bash vlx -- varlock run -- node ./script.js ``` * yarn ```bash yarn exec -- varlock run -- node ./script.js ``` ### Type-safety and IntelliSense [Section titled “Type-safety and IntelliSense”](#type-safety-and-intellisense) To enable type-safety and IntelliSense for your env vars, enable the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) in your `.env.schema`. Note that if your schema was created using `varlock init`, it will include this by default. .env.schema ```diff +# @generateTsTypes(path='env.d.ts') # --- # your config items... ``` *** ## Managing multiple environments [Section titled “Managing multiple environments”](#managing-multiple-environments) Varlock can load multiple *environment-specific* `.env` files (e.g., `.env.development`, `.env.preview`, `.env.production`) by using the [`@currentEnv` root decorator](/reference/root-decorators/#currentenv). **This is different than Vite’s default behaviour, which relies on it’s own [`MODE` flag](https://vite.dev/guide/env-and-mode.html#modes).** Usually this env var will be defaulted to something like `development` in your `.env.schema` file, and you can override it by overriding the value when running commands - for example `APP_ENV=production vite build`. For a JavaScript based project, this will often be done in your `package.json` scripts. package.json ```json { "scripts": { "dev": "vite dev", "test": "APP_ENV=test vitest", "build": "APP_ENV=production vite build", "preview": "APP_ENV=production vite preview", } } ``` In some cases, you could also set the current environment value based on other vars already injected by your CI platform, like the current branch name. See the [environments guide](/guides/environments) for more information. ## Managing sensitive config values [Section titled “Managing sensitive config values”](#managing-sensitive-config-values) Vite uses the `VITE_` prefix to determine which env vars are public (bundled for the browser). Varlock decouples the concept of being *sensitive* from key names, and instead you control this with the [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive) root decorator and the [`@sensitive`](/reference/item-decorators/#sensitive) item decorator. See the [secrets guide](/guides/secrets) for more information. Set a default and explicitly mark items: .env.schema ```diff +# @defaultSensitive=false # --- NON_SECRET_FOO= # sensitive by default # @sensitive SECRET_FOO= ``` Or if you’d like to continue using Vite’s prefix behavior: .env.schema ```diff +# @defaultSensitive=inferFromPrefix('VITE_') # --- FOO= # sensitive VITE_FOO= # non-sensitive, due to prefix ``` Bundling behavior All non-sensitive items are bundled at build time via `ENV`, while `import.meta.env` replacements continue to only include `VITE_`-prefixed items. *** Encrypting the env blob When using `ssrInjectMode: 'resolved-env'`, Varlock injects the fully resolved env data into your SSR build output. By default, this blob is plaintext JSON, meaning anyone with access to the build artifact can read your secrets. You can encrypt it for extra protection (e.g., against sourcemap leaks). See the [encrypted deployments guide](/guides/encrypted-deployments/) for setup instructions. *** ## Reference [Section titled “Reference”](#reference) * [Root decorators reference](/reference/root-decorators) * [Item decorators reference](/reference/item-decorators) * [Functions reference](/reference/functions) * [Vite environment variable docs](https://vite.dev/guide/env-and-mode.html) # 1Password Plugin > Using 1Password with Varlock [![](https://img.shields.io/npm/v/@varlock/1password-plugin?label=%40varlock%2F1password-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/1password-plugin) Our [1Password](https://1password.com/) plugin enables secure loading of values from 1Password vaults using declarative instructions within your `.env` files. For local development, it (optionally) supports authenticating using the local 1Password desktop app, including using biometric unlock. Otherwise, it uses a [service account](https://www.1password.dev/service-accounts/) making it suitable for CI/CD and production environments. This plugin is compatible with any 1Password account type (personal, family, teams, business), but note that [rate limits](https://www.1password.dev/service-accounts/rate-limits/) vary by account type. ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/1password-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/1password-plugin) # # 2. Initialize the plugin - see below for more details on options # @initOp(token=$OP_TOKEN, allowAppAuth=forEnv(dev), account=acmeco) # --- # 3. Add a service account token config item (if applicable) # @type=opServiceAccountToken @sensitive @internal OP_TOKEN= ``` ### Vault setup [Section titled “Vault setup”](#vault-setup) If your secrets are already stored in 1Password, you may not need to do anything. However, if secrets live in a vault that holds other sensitive data, you should create a new vault and move your secrets to it, because **the access system of 1Password is based on vaults, not individual items**. You can create multiple vaults to segment access to different environments, services, etc. This can be done using any 1Password app, the web app, or the CLI. [link](https://support.1password.com/create-share-vaults/#create-a-vault) Remember to grant access to necessary team members, particularly if you plan on using the desktop app auth method during local development, as they will be authenticating as themselves. Vault organization best practices Consider how you want to organize your vaults and service accounts, keeping in mind [best practices](https://support.1password.com/business-security-practices/#access-management-and-the-principle-of-least-privilege). At a minimum, we recommend having a vault for highly sensitive production secrets and another for everything else. ### Service account setup (for deployed environments) [Section titled “Service account setup (for deployed environments)”](#service-account-setup-for-deployed-environments) If you plan on using data from 1Password in deployed environments (CI/CD, production, etc), you will need to create a [service account](https://www.1password.dev/service-accounts/get-started/) to allow machine-to-machine authentication. You could also use a service account for local development, although we recommend using the desktop app auth method described below for convenience. This service account token will now serve as your *secret-zero*, granting access to the rest of your sensitive data stored in 1Password. For local-only workflows, you can secure this value by using [device-local encryption](/guides/local-encryption/). 1. **Create a new service account** and grant access to necessary vault(s). This is a special account used for machine-to-machine communication. This can only be done in the 1Password web interface. Be sure to save the new service account token in another vault so you can find it later. [link](https://www.1password.dev/service-accounts/get-started/) Vault access is set during creation only Vault access rules cannot be edited after creation, so if your vault setup changes, you will need to create new service account(s) and update the tokens. 2. **Wire up the service account token in your config**. Add a config item of type `opServiceAccountToken` to hold the token value, and reference it when initializing the plugin. .env.schema ```diff # @plugin(@varlock/1password-plugin) # @initOp(token=$OP_TOKEN) # --- +# @type=opServiceAccountToken @sensitive @internal OP_TOKEN= ``` Service account tokens are `@internal` by default The `opServiceAccountToken` type is marked [`@internal`](/reference/item-decorators/#internal), so the token is used by varlock to fetch your other secrets but is **not** injected into your application, keeping this secret-zero out of your app’s environment. It still shows (redacted) in `varlock load` output so you can debug it. If your app genuinely needs the token directly, opt out with `@internal=false`. 3. **Set your service account token in deployed environments**. Copy the token value from where you saved it earlier, and set it in deployed environments using your platform’s env var management UI. Be sure to use the same name as you defined in your schema (e.g. `OP_TOKEN`). Ensure service account access is enabled Each vault has a toggle to disable service account access *in general*. It is on by default, so you will likely not need to do anything. [link](https://www.1password.dev/service-accounts/manage-service-accounts/) ### CLI-based service account auth (memory-constrained environments) [Section titled “CLI-based service account auth (memory-constrained environments)”](#cli-based-service-account-auth-memory-constrained-environments) By default, service account tokens use the 1Password JavaScript SDK (which bundles a WASM module, \~400 MB RSS). In memory-constrained environments such as 512 MB containers or VMs, this footprint may be prohibitive. Set `useCliWithServiceAccount=true` to use the `op` CLI binary instead of the SDK while still authenticating headlessly via the service account token: .env.schema ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(token=$OP_TOKEN, useCliWithServiceAccount=true) # --- # @type=opServiceAccountToken @sensitive @internal OP_TOKEN= ``` **Requirements:** 1. **Install the `op` CLI**: [Installation guide](https://www.1password.dev/cli/get-started/) 2. The token referenced by `token=` must resolve to a valid service account token at load time. The `op` binary is significantly lighter than the WASM SDK. Authentication is handled headlessly via `OP_SERVICE_ACCOUNT_TOKEN`, with no desktop app or interactive sign-in needed. Multiple plugin instances configured with different service account tokens are fully isolated from one another. ### Desktop app auth (for local dev) [Section titled “Desktop app auth (for local dev)”](#desktop-app-auth-for-local-dev) During local development, you may find it convenient to skip the service account tokens and instead rely on your local 1Password desktop app (via the [CLI integration](https://www.1password.dev/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration)), including using its biometric unlocking features. 1. **Opt-in while initializing the plugin** .env.schema ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(token=$OP_TOKEN, allowAppAuth=true) ``` You may use other functions to conditionally enable this, for example `forEnv(dev)`. 2. **Specify 1Password account (optional)** .env.schema ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(token=$OP_TOKEN, allowAppAuth=true, account=acmeco) ``` This value is passed through under the `--account` flag to the `op` CLI, and accepts account shorthand, sign-in address, account ID, or user ID. You can run `op account list` to see your available accounts. The shorthand is the subdomain of your `x.1password.com` sign-in address. This is optional, but recommended if you have access to multiple 1Password accounts, to ensure you connect to the correct one. 3. **Ensure the `op` CLI is installed**. [docs](https://www.1password.dev/cli/get-started/) 4. **Enable the desktop app + CLI integration**. [docs](https://www.1password.dev/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration) With this option enabled, if the resolved service account token is empty, we will call out to the `op` cli installed on your machine (it must be in your `$PATH`) and use the auth it provides. With the desktop app integration enabled, it will call out and may trigger biometric verification to unlock. Connecting as yourself Keep in mind that this method is connecting as *YOU* who likely has more access than a tightly scoped service account. Consider only enabling this method for a plugin instance that will be handling non-production secrets. ## Pulling data from 1Password [Section titled “Pulling data from 1Password”](#pulling-data-from-1password) Once the plugin is installed and initialized, you can start adding config items that load values from 1Password using the `op()` resolver function. You can wire up individual items to specific fields in by using [1Password secret references](https://www.1password.dev/cli/secret-references/). ```env-spec DB_PASS=op(op://my-vault/database-password/password) ``` Where to find a secret reference The secret reference for invidivual fields within an item can be found by clicking on the down arrow icon on the field and selecting `Copy Secret Reference`. If you have multiple plugin instances, the `op()` function accepts an optional first parameter to specify which instance id to use. ```env-spec # @initOp(id=dev, token=$OP_TOKEN_DEV, allowAppAuth=true) # @initOp(id=prod, token=$OP_TOKEN_PROD, allowAppAuth=false) # --- DEV_ITEM=op(dev, op://vault-name/item-name/field-name) PROD_ITEM=op(prod, op://vault-name/item-name/field-name) ``` ### Loading 1Password Environments [Section titled “Loading 1Password Environments”](#loading-1password-environments) Use `opLoadEnvironment()` with `@setValuesBulk` to load all variables from a [1Password environment](https://www.1password.dev/environments/) at once, instead of wiring up each secret individually: .env.schema ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(token=$OP_TOKEN, allowAppAuth=forEnv(dev), account=acmeco) # @setValuesBulk(opLoadEnvironment(your-environment-id)) # --- # @type=opServiceAccountToken @sensitive @internal OP_TOKEN= API_KEY= DB_PASSWORD= ``` With a named instance: .env.schema ```env-spec # @initOp(id=prod, token=$OP_TOKEN_PROD, allowAppAuth=false) # @setValuesBulk(opLoadEnvironment(prod, your-environment-id)) ``` Beta CLI required for desktop app auth When using desktop app auth (`allowAppAuth`), the `op environment` command requires a beta version of the 1Password CLI (v2.33.0+). Download it from the [CLI release history](https://app-updates.agilebits.com/product_history/CLI2) (click “show betas”). Service account auth via the SDK does not have this requirement. *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initOp()` [Section titled “@initOp()”](#initop) Initializes an instance of the 1Password plugin, setting up options and authentication. Can be called multiple times to set up different instances. **Key/value args:** * `id` (optional): identifier for this instance, used when multiple instances are needed * `token` (optional): service account token. Should be a reference to a config item of type `opServiceAccountToken`. * `allowAppAuth` (optional): boolean flag to enable authenticating using the local desktop app * `account` (optional): limits the `op` cli to connect to specific 1Password account (shorthand, sign-in address, account ID, or user ID) * `useCliWithServiceAccount` (optional): when `true`, uses the `op` CLI binary instead of the WASM SDK for service account auth. Useful in memory-constrained environments. Requires the `op` CLI to be installed. * `cacheTtl` (optional): when set, resolved values from `op()` and `opLoadEnvironment()` are cached for the specified duration. Accepts the same format as the [`cache()` function](/reference/functions/#cache), for example `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared. Storage follows global [`@cache`](/reference/root-decorators/#cache) mode (`disk` or `memory`), and is disabled when caching is globally disabled or `--skip-cache` is used. For cache mode strategy and CLI cache controls, see the [Caching guide](/guides/caching/). ```env-spec # @initOp(id=notProd, token=$OP_TOKEN, allowAppAuth=forEnv(dev), account=acmeco, cacheTtl=1h) # --- # @type=opServiceAccountToken OP_TOKEN= ``` ### Data types [Section titled “Data types”](#data-types) #### `opServiceAccountToken` [Section titled “opServiceAccountToken”](#opserviceaccounttoken) Represents a [1Password service account token](https://www.1password.dev/service-accounts/). Validation ensures the token is in the correct format, and a link to the 1Password docs is added for convenience. Note that the type itself is marked as `@sensitive`, so adding an explicit `@sensitive` decorator is optional. ```env-spec # @type=opServiceAccountToken OP_TOKEN= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `op()` [Section titled “op()”](#op) Fetches an individual field using a 1Password secret reference **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretReference`: secret reference to fetch value from, in the format `op://vault-name/item-name/field-name` ```env-spec ITEM=op(op://vault-name/item-name/field-name) # example using a plugin instance id ITEM_WITH_INSTANCE_ID=op(prod, op://vault-name/item-name/field-name) ``` #### `opLoadEnvironment()` [Section titled “opLoadEnvironment()”](#oploadenvironment) Load all variables from a [1Password environment](https://www.1password.dev/environments/). Intended for use with `@setValuesBulk`. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `environmentId`: the 1Password environment ID to load variables from ```env-spec # Load all variables from a 1Password environment # @setValuesBulk(opLoadEnvironment(your-environment-id)) # With a named instance # @setValuesBulk(opLoadEnvironment(prod, your-environment-id)) ``` # Akeyless Plugin > Using Akeyless Platform secrets with Varlock [![](https://img.shields.io/npm/v/@varlock/akeyless-plugin?label=%40varlock%2Fakeyless-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/akeyless-plugin) Our Akeyless plugin enables secure loading of secrets from [Akeyless Platform](https://www.akeyless.io/) using declarative instructions within your `.env` files. The plugin uses Akeyless’s REST API with API Key or [OIDC](/guides/oidc/) authentication and supports static, dynamic, and rotated secret types. ## Features [Section titled “Features”](#features) * **API Key authentication** - Simple Access ID + Access Key authentication * **[OIDC authentication](/guides/oidc/)** - Authenticate using platform OIDC tokens (Vercel, GitHub Actions, etc.) * **Static secrets** - Fetch key/value secrets * **Dynamic secrets** - Fetch on-demand generated credentials (database, cloud, etc.) * **Rotated secrets** - Fetch auto-rotated credentials * **JSON key extraction** from secrets using `#` syntax or named `key` parameter * **Path prefixing** with `pathPrefix` option for organized secret management * **Gateway support** - Use a self-hosted [Akeyless Gateway](https://docs.akeyless.io/docs/api-gateway) via custom API URL * **Auto-infer secret name** from environment variable names * Support for multiple Akeyless instances * Automatic token caching and renewal * Lightweight implementation using REST API (no SDK dependencies) For global cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/akeyless-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/akeyless-plugin) # # 2. Initialize the plugin - see below for more details on options # @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY) ``` ### API Key authentication [Section titled “API Key authentication”](#api-key-authentication) The plugin authenticates using an API Key consisting of an Access ID and Access Key: 1. **Create an API Key in Akeyless** (see Akeyless Setup section below) 2. **Wire up the credentials in your config**. Add config items for the Access ID and Access Key, and reference them when initializing the plugin. .env.schema ```env-spec # @plugin(@varlock/akeyless-plugin) # @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY) # --- # @type=akeylessAccessId AKEYLESS_ACCESS_ID= # @type=akeylessAccessKey @sensitive @internal AKEYLESS_ACCESS_KEY= ``` 3. **Set your credentials in deployed environments**. Use your platform’s env var management UI to securely inject these values. ### OIDC authentication [Section titled “OIDC authentication”](#oidc-authentication) For deployments on platforms that issue OIDC tokens (Vercel, GitHub Actions, GitLab CI, Fly.io, GCP), you can use [OIDC workload identity federation](/guides/oidc/) to authenticate without an API key. Varlock auto-detects the platform’s OIDC token and uses it to authenticate via Akeyless’s OIDC access type. **Provider-side setup:** 1. Create an OIDC auth method in Akeyless Console 2. Configure it with the deployment platform’s issuer URL 3. Associate the auth method with an access role that has read permissions **Varlock config:** .env.schema ```env-spec # @plugin(@varlock/akeyless-plugin) # @initAkeyless(oidcAccessId="p-your-access-id") ``` No `accessKey` is needed. The `oidcAccessId` parameter triggers OIDC authentication. For platforms not auto-detected, pass an explicit token via `oidcToken`. ### Using an Akeyless Gateway [Section titled “Using an Akeyless Gateway”](#using-an-akeyless-gateway) If you are running a self-hosted [Akeyless Gateway](https://docs.akeyless.io/docs/api-gateway), provide the gateway URL via the `apiUrl` parameter: .env.schema ```env-spec # @initAkeyless( # accessId=$AKEYLESS_ACCESS_ID, # accessKey=$AKEYLESS_ACCESS_KEY, # apiUrl="https://gateway.example.com:8080" # ) ``` ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to multiple Akeyless instances, register named instances: .env.schema ```env-spec # @initAkeyless(id=prod, accessId=$PROD_ACCESS_ID, accessKey=$PROD_ACCESS_KEY) # @initAkeyless(id=dev, accessId=$DEV_ACCESS_ID, accessKey=$DEV_ACCESS_KEY) # --- PROD_SECRET=akeyless(prod, "/MyApp/Secret") DEV_SECRET=akeyless(dev, "/MyApp/Secret") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `akeyless()` resolver function. ### Static secrets [Section titled “Static secrets”](#static-secrets) Static secrets are simple key/value pairs. This is the default secret type. .env.schema ```env-spec # Fetch a static secret by its full path DB_PASSWORD=akeyless("/MyApp/DB_PASSWORD") # Extract a JSON key from a static secret storing JSON DB_HOST=akeyless("/MyApp/DBConfig#host") # Or use named key parameter DB_PORT=akeyless("/MyApp/DBConfig", key="port") ``` ### Path prefixing [Section titled “Path prefixing”](#path-prefixing) Use `pathPrefix` to automatically prefix all secret paths for better organization: .env.schema ```env-spec # @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY, pathPrefix="/MyApp") # --- # Fetches from "/MyApp/DB_PASSWORD" DB_PASSWORD=akeyless("DB_PASSWORD") # Auto-infer also uses the prefix: fetches from "/MyApp/API_KEY" API_KEY=akeyless() ``` ### Dynamic secrets [Section titled “Dynamic secrets”](#dynamic-secrets) Dynamic secrets generate on-demand credentials (e.g., temporary database credentials, cloud access tokens). Use the `type=dynamic` parameter: .env.schema ```env-spec # Fetch entire dynamic secret as JSON DB_CREDENTIALS=akeyless("/MyApp/DynamicDBSecret", type=dynamic) # Extract a specific key from the dynamic secret response DB_USER=akeyless("/MyApp/DynamicDBSecret#user", type=dynamic) DB_PASS=akeyless("/MyApp/DynamicDBSecret#password", type=dynamic) ``` Multiple items that reference the same dynamic secret path are cached: only one API call is made, and each item extracts its key from the cached response. ### Rotated secrets [Section titled “Rotated secrets”](#rotated-secrets) Rotated secrets are auto-rotated credentials managed by Akeyless. Use the `type=rotated` parameter: .env.schema ```env-spec # Fetch entire rotated secret as JSON DB_ROTATED_CREDS=akeyless("/MyApp/RotatedDBPassword", type=rotated) # Extract individual keys from the rotated secret DB_USER=akeyless("/MyApp/RotatedDBPassword#user", type=rotated) DB_PASS=akeyless("/MyApp/RotatedDBPassword#password", type=rotated) ``` *** ## Akeyless Setup [Section titled “Akeyless Setup”](#akeyless-setup) ### Create an API Key [Section titled “Create an API Key”](#create-an-api-key) 1. **Log in** to the [Akeyless Console](https://console.akeyless.io) 2. **Create an Auth Method**: Go to **Auth Methods** → **New** → **API Key** 3. **Save the credentials**: Copy the generated **Access ID** (starts with `p-`) and **Access Key** ### Create a static secret [Section titled “Create a static secret”](#create-a-static-secret) You can create secrets via the Akeyless CLI or Console: ```bash # Using the Akeyless CLI akeyless create-secret --name "/MyApp/DB_PASSWORD" --value "supersecret" ``` Or in the Console: **Secrets & Keys** → **New** → **Static Secret** ### Set up access permissions [Section titled “Set up access permissions”](#set-up-access-permissions) 1. Go to **Access Roles** in the Akeyless Console 2. Create or edit a role and add rules to grant **read** access to the secrets your application needs 3. Associate the role with your API Key auth method Least privilege principle Only grant access to the specific secret paths your application needs. Avoid granting broad access to all secrets. *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initAkeyless()` [Section titled “@initAkeyless()”](#initakeyless) Initialize an Akeyless plugin instance. **Key/value args:** * `accessId` (optional): Akeyless Access ID (starts with `p-` for API Key auth). Required when using API Key auth. * `accessKey` (optional): Akeyless Access Key. Required when using API Key auth. * `oidcAccessId` (optional): Akeyless Access ID for OIDC authentication (alternative to `accessId`/`accessKey`) * `oidcToken` (optional): Explicit OIDC JWT token (auto-detected from platform if omitted) * `apiUrl` (optional): Akeyless API URL (defaults to `https://api.akeyless.io`). Use this for self-hosted Akeyless Gateway. * `pathPrefix` (optional): Prefix automatically prepended to all secret paths * `id` (optional): Instance identifier for multiple instances * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). Only `static` secrets are cached. `dynamic` and `rotated` secrets change per fetch and are never cached. ```env-spec # @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY, pathPrefix="/MyApp") ``` ### Data types [Section titled “Data types”](#data-types) #### `akeylessAccessId` [Section titled “akeylessAccessId”](#akeylessaccessid) Represents an Akeyless Access ID for API Key authentication. Validates that the value starts with `p-`. ```env-spec # @type=akeylessAccessId AKEYLESS_ACCESS_ID= ``` #### `akeylessAccessKey` [Section titled “akeylessAccessKey”](#akeylessaccesskey) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents an Akeyless Access Key for API Key authentication. This type is marked as `@sensitive`. ```env-spec # @type=akeylessAccessKey AKEYLESS_ACCESS_KEY= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `akeyless()` [Section titled “akeyless()”](#akeyless) Fetch a secret from Akeyless Platform. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretName` (optional): full path to the secret, optionally with `#KEY` to extract a JSON key (e.g., `"/MyApp/Secret#username"`). If omitted, uses the item key (variable name) as the secret name. **Named args:** * `type` (optional): secret type, one of `static` (default), `dynamic`, or `rotated` * `key` (optional): JSON key to extract from the secret value (overrides `#KEY` syntax) **Secret types:** * `static`: Simple key/value secrets (default). If the value is JSON, use `#KEY` or `key=` to extract individual keys. * `dynamic`: On-demand generated credentials (database, cloud, etc.). Returns JSON by default, or extract a specific key with `#KEY` or `key=`. * `rotated`: Auto-rotated credentials managed by Akeyless. Returns JSON by default, or extract a specific key with `#KEY` or `key=`. **Caching:** Multiple items referencing the same secret path (and type) share a single API call. This is especially useful for dynamic and rotated secrets where you need to extract multiple keys from the same response. ```env-spec # Uses item key as secret name (static) DATABASE_URL=akeyless() # Explicit secret path (static) DB_PASSWORD=akeyless("/MyApp/DB_PASSWORD") # Extract JSON key using # syntax DB_HOST=akeyless("/MyApp/DBConfig#host") # Extract JSON key using key= parameter DB_PORT=akeyless("/MyApp/DBConfig", key="port") # Dynamic secret - extract specific keys DB_USER=akeyless("/MyApp/DynamicDB#user", type=dynamic) DB_PASS=akeyless("/MyApp/DynamicDB#password", type=dynamic) # Rotated secret API_KEY=akeyless("/MyApp/RotatedKey#api_key", type=rotated) # With instance ID PROD_SECRET=akeyless(prod, "/MyApp/Secret") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Secret not found [Section titled “Secret not found”](#secret-not-found) * Verify the secret exists in the Akeyless Console * Check the full secret path (e.g., `/MyFolder/MySecret`) * Ensure the path starts with `/` * If using `pathPrefix`, check the combined path is correct ### JSON key not found [Section titled “JSON key not found”](#json-key-not-found) * Verify the key exists in the secret value: check the Akeyless Console for the secret’s content * Key names are case-sensitive * For static secrets, ensure the value is valid JSON when using `#KEY` or `key=` ### Permission denied [Section titled “Permission denied”](#permission-denied) * Check the Access Role associated with your API Key auth method * Ensure the role includes read permission for the secret path * Verify the role is associated with the correct auth method ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * Verify the Access ID starts with `p-` (API Key auth) * Ensure the Access Key matches the Access ID * If using a Gateway, verify the `apiUrl` is correct and reachable * Check if the auth method is active in the Akeyless Console ## Resources [Section titled “Resources”](#resources) * [Akeyless Platform](https://www.akeyless.io/) * [Akeyless Documentation](https://docs.akeyless.io/) * [Akeyless API Key Authentication](https://docs.akeyless.io/docs/api-key) * [Akeyless REST API Reference](https://docs.akeyless.io/reference) * [Akeyless Gateway](https://docs.akeyless.io/docs/api-gateway) # AWS Secrets Manager Plugin > Using AWS Secrets Manager and Systems Manager Parameter Store with Varlock [![](https://img.shields.io/npm/v/@varlock/aws-secrets-plugin?label=%40varlock%2Faws-secrets-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/aws-secrets-plugin) Our AWS plugin enables secure loading of secrets from [AWS Secrets Manager (SM)](https://aws.amazon.com/secrets-manager/) and from [AWS Systems Manager Parameter Store (SSM)](https://aws.amazon.com/systems-manager/features/#Parameter_Store) using declarative instructions within your `.env` files. The plugin automatically integrates with AWS authentication, including IAM roles for AWS-hosted applications, AWS CLI credentials for local development, and explicit credentials for non-AWS environments. ## Features [Section titled “Features”](#features) * **Zero-config authentication** - Automatically uses AWS credentials from your environment * **IAM role support** - No credentials needed for AWS-hosted apps (EC2, ECS, Lambda, etc.) * **AWS CLI authentication** - Works with `aws configure` for local development * **Auto-infer secret/parameter names** from environment variable names * **JSON key extraction** from secrets/parameters using `#` syntax or named `key` parameter * **Name prefixing** with `namePrefix` option for organized secret management * Support for named AWS profiles * Support for explicit credentials * Support for temporary credentials with session tokens * **[OIDC workload identity](/guides/oidc/)** - Authenticate using platform OIDC tokens (Vercel, GitHub Actions, etc.) ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/aws-secrets-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/aws-secrets-plugin) # # 2. Initialize the plugin - see below for more details on options # @initAws(region=us-east-1) ``` ### Authentication options [Section titled “Authentication options”](#authentication-options) The plugin tries authentication methods in this priority order: 1. **OIDC** - If `oidcRoleArn` is provided, uses [OIDC workload identity federation](/guides/oidc/) 2. **Explicit credentials** - If `accessKeyId` and `secretAccessKey` are provided 3. **Named profile** - If `profile` is specified, uses credentials from `~/.aws/credentials` 4. **Default AWS credential chain** - Environment variables → `~/.aws/credentials` → IAM roles ### Automatic authentication (Recommended) [Section titled “Automatic authentication (Recommended)”](#automatic-authentication-recommended) For most use cases, you only need to provide the AWS region: .env.schema ```env-spec # @plugin(@varlock/aws-secrets-plugin) # @initAws(region=us-east-1) ``` **How this works:** * **Local development:** Run `aws configure` → automatically uses AWS CLI credentials * **AWS-hosted apps** (EC2, ECS, Lambda, Fargate): Attach an IAM role → automatically authenticates (no secrets needed!) * **Works everywhere** with zero configuration beyond the region! ### Explicit credentials (For non-AWS environments) [Section titled “Explicit credentials (For non-AWS environments)”](#explicit-credentials-for-non-aws-environments) If you’re deploying outside of AWS (e.g., Azure, GCP, on-premises), wire up IAM credentials: 1. **Create an IAM user** with the necessary permissions (see AWS Setup section below) 2. **Wire up the credentials in your config**. Add config items for the access key and secret key, and reference them when initializing the plugin. .env.schema ```env-spec # @plugin(@varlock/aws-secrets-plugin) # @initAws( # region=us-east-1, # accessKeyId=$AWS_ACCESS_KEY_ID, # secretAccessKey=$AWS_SECRET_ACCESS_KEY # ) # --- # @type=awsAccessKey AWS_ACCESS_KEY_ID= # @type=awsSecretKey @sensitive @internal AWS_SECRET_ACCESS_KEY= ``` 3. **Set your credentials in deployed environments**. Use your platform’s env var management UI to securely inject these values. ### Using named profiles [Section titled “Using named profiles”](#using-named-profiles) Use a specific profile from your `~/.aws/credentials` file: .env.schema ```env-spec # @plugin(@varlock/aws-secrets-plugin) # @initAws(region=us-east-1, profile=production) ``` You can run `aws configure --profile production` to create additional profiles, or manually edit `~/.aws/credentials`: ```ini [default] aws_access_key_id = AKIAIOSFODNN7EXAMPLE aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY [production] aws_access_key_id = AKIAI44QH8DHBEXAMPLE aws_secret_access_key = je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY ``` ### OIDC authentication [Section titled “OIDC authentication”](#oidc-authentication) For deployments on platforms that issue OIDC tokens (Vercel, GitHub Actions, GitLab CI, Fly.io, GCP), you can use [OIDC workload identity federation](/guides/oidc/) to authenticate without any long-lived credentials. Varlock auto-detects the platform’s OIDC token and exchanges it for temporary AWS credentials via STS `AssumeRoleWithWebIdentity`. **Provider-side setup:** 1. Create an IAM OIDC identity provider for your platform: ```bash aws iam create-open-id-connect-provider \ --url https://oidc.vercel.com \ --client-id-list sts.amazonaws.com \ --thumbprint-list ``` 2. Create an IAM role with a trust policy: ```json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.vercel.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.vercel.com:aud": "sts.amazonaws.com" } } }] } ``` 3. Attach your secrets access policy to the role. **Varlock config:** .env.schema ```env-spec # @plugin(@varlock/aws-secrets-plugin) # @initAws(region=us-east-1, oidcRoleArn="arn:aws:iam::123456789012:role/varlock-oidc-role") ``` The `oidcRoleArn` parameter is the ARN of the IAM role to assume. You can optionally set `oidcSessionName` to customize the session name (defaults to `varlock-session`). For platforms not auto-detected, pass an explicit token via `oidcToken`. ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to multiple regions or use different authentication, register multiple named instances: .env.schema ```env-spec # @initAws(id=us, region=us-east-1) # @initAws(id=eu, region=eu-west-1, profile=eu-prod) # --- US_DATABASE_URL=awsSecret(us, "db-connection") EU_DATABASE_URL=awsSecret(eu, "db-connection") ``` ## Loading secrets and parameters [Section titled “Loading secrets and parameters”](#loading-secrets-and-parameters) Once the plugin is installed and initialized, you can start adding config items that load values using the `awsSecret()` and `awsParam()` resolver functions. ### AWS Secrets Manager [Section titled “AWS Secrets Manager”](#aws-secrets-manager) The `awsSecret()` function fetches secrets from AWS Secrets Manager. .env.schema ```env-spec # Auto-infer secret names (DATABASE_URL -> "DATABASE_URL") DATABASE_URL=awsSecret() API_KEY=awsSecret() # Explicit secret names STRIPE_KEY=awsSecret("payments/stripe-secret-key") ``` ### Systems Manager Parameter Store [Section titled “Systems Manager Parameter Store”](#systems-manager-parameter-store) The `awsParam()` function fetches parameters from Parameter Store. .env.schema ```env-spec # Parameters from Parameter Store APP_CONFIG=awsParam("/prod/app/config") FEATURE_FLAGS=awsParam("/prod/features") # Auto-infer parameter names too DATABASE_HOST=awsParam() ``` ### JSON key extraction [Section titled “JSON key extraction”](#json-key-extraction) If your secrets or parameters contain JSON, you can extract specific keys: .env.schema ```env-spec # If "database-creds" contains: {"host": "db.example.com", "password": "secret"} # Using # syntax (shorthand) DB_HOST=awsSecret("database-creds#host") DB_PASSWORD=awsSecret("database-creds#password") # Or use named "key" parameter DB_PORT=awsSecret("database-creds", key="port") ``` ### Name prefixing [Section titled “Name prefixing”](#name-prefixing) Use `namePrefix` to automatically prefix all secret/parameter names for better organization: .env.schema ```env-spec # @initAws(region=us-east-1, namePrefix="prod/api/") # --- # Fetches "prod/api/DATABASE_URL" DATABASE_URL=awsSecret() # Fetches "prod/api/stripe-key" STRIPE_KEY=awsSecret("stripe-key") ``` You can even use dynamic prefixes: .env.schema ```env-spec # @initAws(region=us-east-1, namePrefix="${ENV}/") # --- # In prod: fetches "prod/DATABASE_URL" # In dev: fetches "dev/DATABASE_URL" DATABASE_URL=awsSecret() ``` *** ## AWS Setup [Section titled “AWS Setup”](#aws-setup) ### Required IAM permissions [Section titled “Required IAM permissions”](#required-iam-permissions) Your IAM user or role needs specific permissions to access secrets and parameters. #### For AWS Secrets Manager [Section titled “For AWS Secrets Manager”](#for-aws-secrets-manager) ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["secretsmanager:GetSecretValue"], "Resource": "arn:aws:secretsmanager:*:*:secret:*" } ] } ``` #### For AWS Systems Manager Parameter Store [Section titled “For AWS Systems Manager Parameter Store”](#for-aws-systems-manager-parameter-store) ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["ssm:GetParameter"], "Resource": "arn:aws:ssm:*:*:parameter/*" } ] } ``` Least privilege principle For production, scope down the `Resource` field to only the specific secrets/parameters your application needs. For example: `"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/*"` ### IAM roles for AWS-hosted apps (Recommended) [Section titled “IAM roles for AWS-hosted apps (Recommended)”](#iam-roles-for-aws-hosted-apps-recommended) IAM roles are the AWS-native way to authenticate - no credentials needed! 1. **Create an IAM role** with the necessary permissions and trust policy for your service (EC2, ECS, or Lambda) 2. **Attach the role to your application** * **EC2:** Create an instance profile and attach it to your instance * **ECS:** Set the `taskRoleArn` in your task definition * **Lambda:** Set the execution role in your function configuration 3. **That’s it!** Your app will automatically authenticate using the IAM role. See the [AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) for detailed instructions on creating and attaching IAM roles. ### IAM user for non-AWS environments [Section titled “IAM user for non-AWS environments”](#iam-user-for-non-aws-environments) 1. **Create an IAM user** ```bash aws iam create-user --user-name varlock-secrets-reader ``` 2. **Attach the permissions policy** ```bash aws iam put-user-policy \ --user-name varlock-secrets-reader \ --policy-name secrets-access \ --policy-document file://policy.json ``` 3. **Create access credentials** ```bash aws iam create-access-key --user-name varlock-secrets-reader ``` Save the `AccessKeyId` and `SecretAccessKey` from the output - you’ll need them for your deployments. ### Configure AWS CLI for local development [Section titled “Configure AWS CLI for local development”](#configure-aws-cli-for-local-development) 1. **Run `aws configure`** ```bash aws configure # AWS Access Key ID: [your key] # AWS Secret Access Key: [your secret] # Default region name: us-east-1 # Default output format: json ``` 2. **Test the configuration** ```bash aws sts get-caller-identity ``` *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initAws()` [Section titled “@initAws()”](#initaws) Initialize an AWS plugin instance for accessing Secrets Manager and Parameter Store. **Key/value args:** * `region` (required): AWS region (e.g., `us-east-1`, `eu-west-1`) * `namePrefix` (optional): Prefix automatically prepended to all secret/parameter names * `accessKeyId` (optional): AWS access key ID for explicit authentication * `secretAccessKey` (optional): AWS secret access key for explicit authentication * `sessionToken` (optional): AWS session token for temporary credentials * `profile` (optional): Named profile from `~/.aws/credentials` * `oidcRoleArn` (optional): IAM role ARN for OIDC workload identity authentication * `oidcSessionName` (optional): Session name for OIDC role assumption (defaults to `varlock-session`) * `oidcToken` (optional): Explicit OIDC JWT token (auto-detected from platform if omitted) * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). * `id` (optional): Instance identifier for multiple instances ```env-spec # @initAws(region=us-east-1, namePrefix="prod/api/", cacheTtl="1h") ``` ### Data types [Section titled “Data types”](#data-types) #### `awsAccessKey` [Section titled “awsAccessKey”](#awsaccesskey) Represents an AWS access key ID (20-character alphanumeric string). Note that the type itself is marked as `@sensitive`, so adding an explicit `@sensitive` decorator is optional. ```env-spec # @type=awsAccessKey AWS_ACCESS_KEY_ID= ``` #### `awsSecretKey` [Section titled “awsSecretKey”](#awssecretkey) Represents an AWS secret access key (40-character string). This type is marked as `@sensitive`. ```env-spec # @type=awsSecretKey AWS_SECRET_ACCESS_KEY= ``` Does your app use these credentials too? The examples here mark the credential [`@internal`](/reference/item-decorators/#internal) so it’s used by varlock to fetch your secrets but **not** injected into your app. This is the recommended posture when varlock is the only consumer. Unlike the dedicated secrets-manager tokens, AWS credentials are **not** `@internal` by default (they’re commonly read directly by the AWS SDK), so if you need the credential at runtime for other purposes, opt out with `@internal=false` to keep it injected: ```env-spec # @type=awsSecretKey @internal=false AWS_SECRET_ACCESS_KEY= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `awsSecret()` [Section titled “awsSecret()”](#awssecret) Fetch a secret from AWS Secrets Manager. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretId` (optional): secret name, ARN, or name with JSON key using `#` syntax. If omitted, uses the variable name. * `key` (optional, named parameter): JSON key to extract from the secret value ```env-spec # Auto-infer secret name DATABASE_URL=awsSecret() # Explicit secret name STRIPE_KEY=awsSecret("payments/stripe-key") # Extract JSON key (shorthand) DB_HOST=awsSecret("database-creds#host") # Extract JSON key (named parameter) DB_PORT=awsSecret("database-creds", key="port") # With instance ID US_SECRET=awsSecret(us, "my-secret") ``` #### `awsParam()` [Section titled “awsParam()”](#awsparam) Fetch a parameter from AWS Systems Manager Parameter Store. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `parameterName` (optional): parameter name/path or name with JSON key using `#` syntax. If omitted, uses the variable name. * `key` (optional, named parameter): JSON key to extract from the parameter value ```env-spec # Auto-infer parameter name DATABASE_HOST=awsParam() # Explicit parameter path APP_CONFIG=awsParam("/prod/app/config") # Extract JSON key DB_CREDS=awsParam("/prod/db/creds#password") # With instance ID EU_CONFIG=awsParam(eu, "/prod/config") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Secret not found [Section titled “Secret not found”](#secret-not-found) * Verify the secret exists: `aws secretsmanager list-secrets --query 'SecretList[?Name==\`my-secret\`]’\` * Check you’re using the correct region * Ensure the secret name matches exactly (including any prefix) ### Parameter not found [Section titled “Parameter not found”](#parameter-not-found) * Verify the parameter exists: `aws ssm describe-parameters --parameter-filters "Key=Name,Values=/my/param"` * Check you’re using the correct region * Parameter Store paths are case-sensitive ### Permission denied [Section titled “Permission denied”](#permission-denied) * Check your IAM permissions: Test with `aws sts get-caller-identity` to see which identity you’re using * For IAM roles on EC2/ECS/Lambda: Verify the role is attached and has the required permissions * Ensure the IAM policy includes `secretsmanager:GetSecretValue` and/or `ssm:GetParameter` ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * **Local dev:** Run `aws configure` or ensure `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are set * **AWS-hosted apps:** Verify IAM role is attached * **Other environments:** Verify credentials are correct and properly injected * Test credentials: `aws sts get-caller-identity` ### JSON parsing errors [Section titled “JSON parsing errors”](#json-parsing-errors) * Verify your secret/parameter contains valid JSON * Check that the key you’re extracting exists in the JSON * Test manually: `aws secretsmanager get-secret-value --secret-id my-secret --query SecretString --output text | jq .` ## Resources [Section titled “Resources”](#resources) * [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) * [AWS Systems Manager Parameter Store](https://aws.amazon.com/systems-manager/features/#Parameter_Store) * [IAM Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) * [AWS SDK for JavaScript](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/) # Azure Key Vault Plugin > Using Azure Key Vault with Varlock [![](https://img.shields.io/npm/v/@varlock/azure-key-vault-plugin?label=%40varlock%2Fazure-key-vault-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/azure-key-vault-plugin) Our [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault) plugin enables secure loading of secrets from Azure Key Vault using declarative instructions within your `.env` files. The plugin automatically integrates with Azure authentication, including Managed Identity for Azure-hosted applications, Azure CLI credentials for local development, and service principal credentials for non-Azure environments. ## Features [Section titled “Features”](#features) * **Zero-config authentication** - Just provide your vault URL, authentication happens automatically * **Managed Identity support** - No credentials needed for Azure-hosted apps (App Service, Container Instances, VMs, Functions, AKS) * **Azure CLI authentication** - Works with `az login` for local development * **Auto-infer secret names** from environment variable names (e.g., `DATABASE_URL` → `database-url`) * Support for service principal credentials (for non-Azure environments) * Support for versioned secrets * Automatic token caching and renewal * **[OIDC workload identity](/guides/oidc/)** - Authenticate using platform OIDC tokens (Vercel, GitHub Actions, etc.) * Lightweight implementation using REST API (47 KB bundle, no heavy Azure SDK dependencies) For global cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/azure-key-vault-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/azure-key-vault-plugin) # # 2. Initialize the plugin - see below for more details on options # @initAzure(vaultUrl="https://my-vault.vault.azure.net/") ``` ### Authentication options [Section titled “Authentication options”](#authentication-options) The plugin tries authentication methods in this priority order: 1. **Service Principal** - If all three credentials (`tenantId`, `clientId`, `clientSecret`) are provided 2. **OIDC federated credentials** - If `tenantId` and `clientId` are provided without `clientSecret`, uses [OIDC workload identity federation](/guides/oidc/) 3. **Managed Identity** - Automatically used when running on Azure infrastructure 4. **Azure CLI** - Falls back to `az login` for local development ### Automatic authentication (Recommended) [Section titled “Automatic authentication (Recommended)”](#automatic-authentication-recommended) For most use cases, you only need to provide the vault URL: .env.schema ```env-spec # @plugin(@varlock/azure-key-vault-plugin) # @initAzure(vaultUrl="https://my-vault.vault.azure.net/") ``` **How this works:** * **Local development:** Run `az login` → automatically uses Azure CLI credentials * **Azure-hosted apps** (App Service, Container Instances, VMs, Functions, AKS): Enable Managed Identity → automatically authenticates (no secrets needed!) * **Works everywhere** with zero configuration beyond the vault URL! Finding your vault URL Run `az keyvault show --name my-vault --query properties.vaultUri -o tsv` to get your vault URL. ### Service principal credentials (For non-Azure environments) [Section titled “Service principal credentials (For non-Azure environments)”](#service-principal-credentials-for-non-azure-environments) If you’re deploying outside of Azure (e.g., AWS, GCP, on-premises), wire up service principal credentials: 1. **Create a service principal** with the necessary permissions (see Azure Setup section below) 2. **Wire up the credentials in your config**. Add config items for the tenant ID, client ID, and client secret, and reference them when initializing the plugin. .env.schema ```env-spec # @plugin(@varlock/azure-key-vault-plugin) # @initAzure( # vaultUrl="https://my-vault.vault.azure.net/", # tenantId=$AZURE_TENANT_ID, # clientId=$AZURE_CLIENT_ID, # clientSecret=$AZURE_CLIENT_SECRET # ) # --- # @type=azureTenantId @sensitive AZURE_TENANT_ID= # @type=azureClientId @sensitive AZURE_CLIENT_ID= # @type=azureClientSecret @sensitive @internal AZURE_CLIENT_SECRET= ``` 3. **Set your credentials in deployed environments**. Use your platform’s env var management UI to securely inject these values. ### OIDC federated credentials [Section titled “OIDC federated credentials”](#oidc-federated-credentials) For deployments on platforms that issue OIDC tokens (Vercel, GitHub Actions, GitLab CI, Fly.io, GCP), you can use [OIDC workload identity federation](/guides/oidc/) to authenticate without a client secret. Varlock auto-detects the platform’s OIDC token and exchanges it for an Azure access token via federated credential authentication. **Provider-side setup:** 1. Create an App Registration (or use an existing one) 2. Add a federated credential: ```bash az ad app federated-credential create \ --id \ --parameters '{ "name": "vercel-oidc", "issuer": "https://oidc.vercel.com", "subject": "", "audiences": ["api://AzureADTokenExchange"] }' ``` 3. Grant the App Registration “Key Vault Secrets User” role on your vault. **Varlock config:** .env.schema ```env-spec # @plugin(@varlock/azure-key-vault-plugin) # @initAzure( # vaultUrl="https://my-vault.vault.azure.net/", # tenantId=$AZURE_TENANT_ID, # clientId=$AZURE_CLIENT_ID # ) ``` When `clientSecret` is omitted but `tenantId` and `clientId` are provided, the plugin automatically uses OIDC federated credentials. For platforms not auto-detected, pass an explicit token via `oidcToken`. ### Multiple vaults [Section titled “Multiple vaults”](#multiple-vaults) If you need to connect to multiple vaults, but never at the same time, you can alter the vault URL using a function: .env.schema ```env-spec # @plugin(@varlock/azure-key-vault-plugin) # @initAzure(vaultUrl="https://my-vault-${ENV}.vault.azure.net/") ``` Or if you need to connect to multiple vaults simultaneously, register multiple named instances: .env.schema ```env-spec # @initAzure(id=prod, vaultUrl="https://my-vault-prod.vault.azure.net/") # @initAzure(id=dev, vaultUrl="https://my-vault-dev.vault.azure.net/") # --- PROD_SECRET=azureSecret(prod, "database-url") DEV_SECRET=azureSecret(dev, "database-url") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `azureSecret()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) The `azureSecret()` function fetches secrets from Azure Key Vault. .env.schema ```env-spec # Auto-infer secret names (DATABASE_URL -> "database-url") DATABASE_URL=azureSecret() API_KEY=azureSecret() # Explicit secret names CUSTOM_SECRET=azureSecret("my-custom-secret-name") ``` Secret name conversion Azure Key Vault uses hyphens instead of underscores. When auto-inferring, the plugin automatically converts `DATABASE_URL` to `database-url` to match Azure’s naming convention. ### Versioned secrets [Section titled “Versioned secrets”](#versioned-secrets) You can fetch specific versions of secrets by appending `@version` to the secret name: .env.schema ```env-spec # Fetch latest version (default) API_KEY=azureSecret("api-key") # Fetch specific version API_KEY_V1=azureSecret("api-key@abc123def456") ``` *** ## Azure Setup [Section titled “Azure Setup”](#azure-setup) ### Required permissions [Section titled “Required permissions”](#required-permissions) Your managed identity, service principal, or user needs one of: * **Access Policy**: “Get” permission for secrets * **RBAC**: “Key Vault Secrets User” role ### Managed Identity for Azure-hosted apps (Recommended) [Section titled “Managed Identity for Azure-hosted apps (Recommended)”](#managed-identity-for-azure-hosted-apps-recommended) Managed Identity is the Azure-native way to authenticate - no credentials needed! 1. **Enable system-assigned managed identity** for your Azure resource ```bash # For App Service az webapp identity assign --name my-app --resource-group my-rg # For Container Instance az container create --assign-identity --name my-container ... # For VM az vm identity assign --name my-vm --resource-group my-rg ``` 2. **Grant Key Vault access to the identity** Get the identity’s principal ID: ```bash PRINCIPAL_ID=$(az webapp identity show --name my-app --resource-group my-rg --query principalId -o tsv) ``` Then grant access using either RBAC or Access Policy: **Option A: RBAC (Recommended)** ```bash az role assignment create \ --role "Key Vault Secrets User" \ --assignee $PRINCIPAL_ID \ --scope /subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/ ``` **Option B: Access Policy** ```bash az keyvault set-policy \ --name my-vault \ --object-id $PRINCIPAL_ID \ --secret-permissions get ``` 3. **That’s it!** Your app will automatically authenticate using Managed Identity. ### Service principal for non-Azure environments [Section titled “Service principal for non-Azure environments”](#service-principal-for-non-azure-environments) 1. **Create a service principal** ```bash az ad sp create-for-rbac --name "varlock-keyvault-reader" ``` Save the `appId`, `password`, and `tenant` from the output. 2. **Grant Key Vault access** **Option A: RBAC (Recommended)** ```bash az role assignment create \ --role "Key Vault Secrets User" \ --assignee \ --scope /subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/ ``` **Option B: Access Policy** ```bash az keyvault set-policy \ --name my-vault \ --spn \ --secret-permissions get ``` ### Azure CLI for local development [Section titled “Azure CLI for local development”](#azure-cli-for-local-development) 1. **Install the Azure CLI** if you haven’t already: [Installation guide](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) 2. **Log in to Azure** ```bash az login ``` 3. **Verify your identity** ```bash az account show ``` 4. **Grant Key Vault access to your user account** (if needed) ```bash az keyvault set-policy \ --name my-vault \ --upn your-email@domain.com \ --secret-permissions get ``` *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initAzure()` [Section titled “@initAzure()”](#initazure) Initialize an Azure Key Vault plugin instance for accessing secrets. **Key/value args:** * `vaultUrl` (required): Azure Key Vault URL (e.g., `https://my-vault.vault.azure.net/`) * `tenantId` (optional): Azure AD tenant ID (directory ID) * `clientId` (optional): Service principal application (client) ID * `clientSecret` (optional): Service principal client secret (password) * `oidcToken` (optional): Explicit OIDC JWT token (auto-detected from platform if omitted) * `id` (optional): Instance identifier for multiple vaults * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). ```env-spec # @initAzure(vaultUrl="https://my-vault.vault.azure.net/") ``` ### Data types [Section titled “Data types”](#data-types) #### `azureTenantId` [Section titled “azureTenantId”](#azuretenantid) Represents an Azure AD tenant ID (UUID format). This type is marked as `@sensitive`. ```env-spec # @type=azureTenantId AZURE_TENANT_ID= ``` #### `azureClientId` [Section titled “azureClientId”](#azureclientid) Represents a service principal application (client) ID (UUID format). This type is marked as `@sensitive`. ```env-spec # @type=azureClientId AZURE_CLIENT_ID= ``` #### `azureClientSecret` [Section titled “azureClientSecret”](#azureclientsecret) Represents a service principal client secret (password). This type is marked as `@sensitive`. ```env-spec # @type=azureClientSecret AZURE_CLIENT_SECRET= ``` Does your app use these credentials too? The examples here mark the credential [`@internal`](/reference/item-decorators/#internal) so it’s used by varlock to fetch your secrets but **not** injected into your app. This is the recommended posture when varlock is the only consumer. Unlike the dedicated secrets-manager tokens, Azure credentials are **not** `@internal` by default (they’re commonly read directly by the Azure SDK), so if you need the credential at runtime for other purposes, opt out with `@internal=false` to keep it injected: ```env-spec # @type=azureClientSecret @internal=false AZURE_CLIENT_SECRET= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `azureSecret()` [Section titled “azureSecret()”](#azuresecret) Fetch a secret from Azure Key Vault. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretName` (optional): secret name or name with version using `@` syntax. If omitted, uses the variable name (converted to kebab-case). ```env-spec # Auto-infer secret name (DATABASE_URL -> "database-url") DATABASE_URL=azureSecret() # Explicit secret name CUSTOM_SECRET=azureSecret("my-custom-secret") # Specific version API_KEY_V1=azureSecret("api-key@abc123def456") # With instance ID PROD_SECRET=azureSecret(prod, "database-url") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Secret not found [Section titled “Secret not found”](#secret-not-found) * Verify the secret exists: `az keyvault secret list --vault-name my-vault` * Remember: Azure uses hyphens, not underscores (use `database-url` not `database_url`) * Check for typos in the secret name ### Permission denied [Section titled “Permission denied”](#permission-denied) * Check your RBAC role: `az role assignment list --assignee --scope ` * Or check access policies: `az keyvault show --name my-vault --query properties.accessPolicies` * Ensure your identity has “Get” permission for secrets ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * **Local dev:** Run `az login` and ensure service principal env vars are empty * **Azure-hosted apps:** Verify Managed Identity is enabled and has Key Vault permissions * **Other environments:** Verify service principal credentials are correct and properly injected * Test identity: `az account show` ### Vault not accessible [Section titled “Vault not accessible”](#vault-not-accessible) * Verify the vault URL is correct * Check network access: Ensure firewall rules allow access from your IP/resource * Verify the vault exists in the specified subscription ## Resources [Section titled “Resources”](#resources) * [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault) * [Managed Identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) * [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/) * [Key Vault Access Policies](https://learn.microsoft.com/en-us/azure/key-vault/general/assign-access-policy) # Bitwarden Plugin > Using Bitwarden Secrets Manager and Password Manager (and Vaultwarden) with Varlock [![](https://img.shields.io/npm/v/@varlock/bitwarden-plugin?label=%40varlock%2Fbitwarden-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/bitwarden-plugin) Our Bitwarden plugin enables secure loading of secrets from Bitwarden using declarative instructions within your `.env` files. It supports two distinct Bitwarden products: * **[Secrets Manager](https://bitwarden.com/products/secrets-manager/)**: programmatic secret storage, accessed via machine account access tokens over the REST API. Best for CI/CD and production. Uses `@initBitwarden()` + `bitwarden()`. * **[Password Manager](https://bitwarden.com/products/personal/)** (and self-hosted **[Vaultwarden](https://github.com/dani-garcia/vaultwarden)**): your personal/team password vault, accessed via the `bw` CLI. Best for local development. Uses `@initBwp()` + `bwp()`. ## Features [Section titled “Features”](#features) * **Zero-config authentication** - Just provide your machine account access token * **UUID-based secret access** - Fetch secrets by their unique identifiers * **Self-hosted Bitwarden support** - Configure custom API and identity URLs * **Multiple instances** - Connect to different organizations or self-hosted instances * **Helpful error handling** with resolution tips * **Lightweight implementation** using REST API (48 KB bundle, no native SDK dependencies) ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/bitwarden-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/bitwarden-plugin) # # 2. Initialize the plugin - see below for more details on options # @initBitwarden(accessToken=$BITWARDEN_ACCESS_TOKEN) # --- # 3. Add a machine account access token config item # @type=bitwardenAccessToken @sensitive @internal BITWARDEN_ACCESS_TOKEN= ``` ### Machine account setup [Section titled “Machine account setup”](#machine-account-setup) 1. **Create a machine account** in your Bitwarden organization Navigate to your Bitwarden organization’s **Secrets Manager** → **Machine accounts** → Click **New machine account**. Provide a name (e.g., “Production App”) and save it. 2. **Copy the access token** (displayed only once!) After creating the machine account, you’ll see an **Access token**. Copy it immediately - it will only be displayed once. Save the token securely Store the access token securely. You won’t be able to see it again after this step! 3. **Grant access to secrets** Grant your machine account access to the specific projects or secrets you need. **Via Projects:** * Create or select a project in Secrets Manager * Add secrets to the project * Grant your machine account access to the project **Direct Secret Access:** * Navigate to a specific secret * Click **Access** * Add your machine account with “Can read” permissions 4. **Wire up the token in your config** .env.schema ```env-spec # @plugin(@varlock/bitwarden-plugin) # @initBitwarden(accessToken=$BITWARDEN_ACCESS_TOKEN) # --- # @type=bitwardenAccessToken @sensitive @internal BITWARDEN_ACCESS_TOKEN= ``` 5. **Set your access token in environments** Use your CI/CD system or platform’s env var management to securely inject the `BITWARDEN_ACCESS_TOKEN` value. Permission levels Machine accounts can have *Can read* (retrieve secrets only) or *Can read, write* (retrieve, create, and edit secrets) permissions. For most use cases, *Can read* is sufficient. ### Self-hosted Bitwarden [Section titled “Self-hosted Bitwarden”](#self-hosted-bitwarden) For self-hosted Bitwarden instances, you’ll need to provide both the API and identity URLs: .env.schema ```env-spec # @plugin(@varlock/bitwarden-plugin) # @initBitwarden( # accessToken=$BITWARDEN_ACCESS_TOKEN, # apiUrl="https://bitwarden.yourcompany.com/api", # identityUrl="https://bitwarden.yourcompany.com/identity" # ) ``` ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to multiple organizations or instances, register multiple named instances: .env.schema ```env-spec # @initBitwarden(id=prod, accessToken=$PROD_ACCESS_TOKEN) # @initBitwarden(id=dev, accessToken=$DEV_ACCESS_TOKEN) # --- PROD_SECRET=bitwarden(prod, "11111111-1111-1111-1111-111111111111") DEV_SECRET=bitwarden(dev, "22222222-2222-2222-2222-222222222222") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `bitwarden()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) Fetch secrets by their UUID: .env.schema ```env-spec # Fetch secrets by UUID DATABASE_URL=bitwarden("12345678-1234-1234-1234-123456789abc") API_KEY=bitwarden("87654321-4321-4321-4321-cba987654321") ``` Finding secret UUIDs To find a secret’s UUID: 1. Open your Bitwarden Secrets Manager 2. Navigate to the secret 3. Copy the UUID from the URL or secret details (format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) ### Multiple instances [Section titled “Multiple instances”](#multiple-instances-1) If you have multiple plugin instances, specify which instance to use: .env.schema ```env-spec PROD_ITEM=bitwarden(prod, "11111111-1111-1111-1111-111111111111") DEV_ITEM=bitwarden(dev, "22222222-2222-2222-2222-222222222222") ``` *** ## Bitwarden Setup [Section titled “Bitwarden Setup”](#bitwarden-setup) ### Create a machine account [Section titled “Create a machine account”](#create-a-machine-account) Machine accounts provide programmatic access to Bitwarden Secrets Manager. 1. **Log in to your Bitwarden organization** web vault 2. **Navigate to Secrets Manager → Machine accounts** 3. **Click “New machine account”** 4. **Provide a name** (e.g., “Production App”) 5. **Copy the Access token** (shown only once!) 6. **Grant access** to specific projects or secrets **Permission Levels:** * **Can read** - Retrieve secrets only (recommended for most use cases) * **Can read, write** - Retrieve, create, and edit secrets Access token security Store the access token securely - it will only be displayed once during creation! ### Grant access to secrets [Section titled “Grant access to secrets”](#grant-access-to-secrets) **Via Projects (Recommended):** 1. Create or select a project in Secrets Manager 2. Add secrets to the project 3. Grant your machine account access to the project This approach makes it easier to manage access to multiple secrets at once. **Direct Secret Access:** 1. Navigate to a specific secret 2. Click **Access** 3. Add your machine account with appropriate permissions *** ## Password Manager / Vaultwarden [Section titled “Password Manager / Vaultwarden”](#password-manager--vaultwarden) Separate from Secrets Manager, you can also load values from your personal or team **Bitwarden Password Manager** vault, or a self-hosted **Vaultwarden** server. This path uses the Bitwarden [`bw` CLI](https://bitwarden.com/help/cli/) rather than the REST API, so it’s best suited to local development. varlock acquires and caches a CLI **session token** for you: on first load it runs `bw unlock` (prompting for your master password), then caches the token in varlock’s encrypted cache and reuses it on subsequent loads. You no longer need to manually unlock and paste a session token into a `.env` file. By default the token is cached indefinitely (`sessionTtl="forever"`). This is safe because the cached token is encrypted at rest with varlock’s local key (biometric-gated with Touch ID / Windows Hello on platforms with a secure enclave), so reading it requires *you*. Note the `bw` CLI session does **not** auto-lock on system sleep/lock/restart (those are app-only vault-timeout settings), so by default a fresh master-password unlock is only needed when the session is explicitly invalidated (`bw lock`/`bw logout`, an external `bw unlock`, or an account policy). Set a shorter `sessionTtl` if you want varlock to force periodic master-password re-auth. ### Prerequisites [Section titled “Prerequisites”](#prerequisites) 1. **Install the `bw` CLI** from a trusted source. Install from trusted sources only Install the official CLI via a trusted channel and keep it up to date. Supply-chain incidents have affected the broader Bitwarden CLI ecosystem in the past. varlock never installs `bw` for you; it only invokes whatever `bw` is already on your `PATH`. ```bash # macOS brew install bitwarden-cli # Linux snap install bw # Windows choco install bitwarden-cli ``` Or download the official binary from the [Bitwarden CLI docs](https://bitwarden.com/help/cli/). 2. **Log in once** with your account: ```bash bw login # For Vaultwarden / self-hosted, point the CLI at your server first: # bw config server https://vault.yourcompany.com ``` varlock handles unlocking from here on, so you don’t need to run `bw unlock` yourself. ### Basic usage [Section titled “Basic usage”](#basic-usage-1) .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/bitwarden-plugin) # # 2. Initialize the Password Manager instance (no token needed) # @initBwp() # --- # 3. Load values from vault items by name or UUID DATABASE_PASSWORD=bwp("Production DB") ``` On the first `varlock load`, you’ll be prompted for your master password once; the resulting session token is cached and reused (`sessionTtl`, default `forever`). The `bw` CLI session does **not** lock on system sleep, lock, or restart (those vault-timeout settings apply to the Bitwarden apps and extension, not the CLI), so in practice you re-enter the master password only when the session is explicitly invalidated (see below) or when you set a shorter `sessionTtl`. Using `bw` outside varlock can invalidate the cached session Each `bw unlock` (and `bw login`) issues a new session key and **invalidates any previously issued ones**; `bw lock` / `bw logout` invalidate them too. So if you run those commands in your own scripts or shell (or keep a `BW_SESSION` exported elsewhere), varlock’s cached token gets invalidated (and vice-versa: varlock unlocking invalidates your shell’s `BW_SESSION`). Plain reads (`bw get`, `bw list`, `bw sync`) are fine and don’t invalidate anything. This is not fatal: the next `varlock` load detects the stale session and re-unlocks once, at the cost of one extra master-password prompt. If you frequently `bw unlock` outside varlock, consider letting varlock own the unlock (don’t re-unlock manually) or pass a shared `sessionToken=$BWP_SESSION` so both sides use the same token. ### Selecting a field [Section titled “Selecting a field”](#selecting-a-field) By default `bwp()` returns the item’s **password**. Use the `field` argument to fetch a different field. Standard fields are `password`, `username`, `notes`, `totp`, and `uri`; any other name is matched against the item’s custom fields (case-insensitive). .env.schema ```env-spec DB_USER=bwp("Production DB", field=username) DB_PASSWORD=bwp("Production DB", field=password) # the stored TOTP secret/seed, not a generated 6-digit code (see note below) API_TOTP_SEED=bwp("Some Service", field=totp) # custom field on the item STRIPE_KEY=bwp("Stripe", field="secret-key") ``` `totp` returns the secret, not a code `field=totp` returns the **stored TOTP secret/seed** (a base32 key or `otpauth://` URI), not a generated 6-digit code. It is read verbatim from `bw get item`, the same way every other field is. There is no field that returns a live code: a one-time code rotates every \~30 seconds, but config is resolved once at load time, so a captured code would be stale before it could be used. ### Caching and TTLs [Section titled “Caching and TTLs”](#caching-and-ttls) .env.schema ```env-spec # @initBwp(sessionTtl="1h", cacheTtl="5m") ``` * `sessionTtl` (default `forever`): how long the unlocked CLI session token is cached before varlock forces a fresh `bw unlock`. `forever` keeps the token until the CLI session is invalidated externally (`bw lock`/`bw logout`, an external `bw unlock`, or account policy), at which point the next load re-unlocks automatically; set e.g. `"1h"` to force periodic master-password re-auth. * `cacheTtl` (optional): additionally cache the **resolved item values**, avoiding a `bw` invocation per value within the window. See the [Caching guide](/guides/caching/). ### Non-interactive loads [Section titled “Non-interactive loads”](#non-interactive-loads) Interactive unlock needs a TTY, so a non-interactive load (a dev server started by a tool that has no TTY, an editor task runner, etc.) can’t show the master-password prompt. How you fix it depends on the environment: **Local dev, using auto-unlock**: if you’re letting varlock unlock the vault itself (no `sessionToken` / `masterPassword` configured on `@initBwp()`), run `varlock load` once in a regular terminal (`npx varlock load`, `pnpm exec varlock load`, etc.). That performs the interactive unlock and caches the session token in varlock’s encrypted cache, so subsequent non-interactive commands reuse it without prompting (until the session is invalidated, see `sessionTtl` above). This only applies to the auto-unlock flow. If you’ve configured `sessionToken=` or `masterPassword=`, no interactive `varlock load` is needed. **CI / truly headless**: there’s no terminal to unlock from, so provide credentials explicitly instead: .env.schema ```env-spec # Option A: pass a pre-obtained session token (from `bw unlock --raw`) # @initBwp(sessionToken=$BWP_SESSION) # Option B: let varlock unlock non-interactively with a master password # @initBwp(masterPassword=$BW_MASTER_PASSWORD) # --- # @type=bwSessionToken @sensitive @internal BWP_SESSION= ``` ### Multiple accounts / servers [Section titled “Multiple accounts / servers”](#multiple-accounts--servers) The `bw` CLI keeps **one logged-in account per data directory**, so to read from more than one account or server (e.g. personal + work, or bitwarden.com + a self-hosted Vaultwarden) you give each instance its own data dir via `appDataDir`. Set up each dir once with the `bw` CLI (`bw config server …` if needed, then `bw login`), then point an instance at it. Since a data-dir path is specific to your machine, reference it from a per-developer value rather than hardcoding it: .env.schema ```env-spec # @initBwp(id=personal, appDataDir=$BWP_PERSONAL_DIR) # @initBwp(id=work, appDataDir=$BWP_WORK_DIR) # --- # bw data dirs, set per-machine in .env.local (a leading ~ is expanded) BWP_PERSONAL_DIR= BWP_WORK_DIR= PERSONAL_TOKEN=bwp(personal, "My Token") WORK_DB=bwp(work, "Work DB", field=password) ``` varlock unlocks and caches a session token **per account** (keyed by `appDataDir`), so the two accounts never share or invalidate each other’s sessions. `appDataDir` is machine-specific A data-dir path points at a directory on *your* machine, so avoid hardcoding it in a shared `.env.schema`. There’s no way to override an already-declared instance’s settings from another file, so make the **value** local rather than the instance: reference `appDataDir=$BWP_WORK_DIR` and set that var in your (gitignored) `.env.local`. If the whole multi-account workflow is personal, you can instead declare the entire `@initBwp(...)` in `.env.local`; root decorators are honored there too. Caution Without distinct `appDataDir`s, multiple instances all point at the same global `bw` account, so `bwp(personal, …)` and `bwp(work, …)` would read the *same* vault. The `appDataDir` is what makes them separate. (This differs from Secrets Manager, where each instance has its own `accessToken`.) *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initBitwarden()` [Section titled “@initBitwarden()”](#initbitwarden) Initialize a Bitwarden Secrets Manager plugin instance for accessing secrets. **Key/value args:** * `accessToken` (required): Machine account access token. Should be a reference to a config item of type `bitwardenAccessToken`. * `apiUrl` (optional): API URL for self-hosted Bitwarden (defaults to `https://api.bitwarden.com`) * `identityUrl` (optional): Identity service URL for self-hosted Bitwarden (defaults to `https://identity.bitwarden.com`) * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). * `id` (optional): Instance identifier for multiple instances ```env-spec # @initBitwarden(accessToken=$BITWARDEN_ACCESS_TOKEN) # --- # @type=bitwardenAccessToken @sensitive @internal BITWARDEN_ACCESS_TOKEN= ``` #### `@initBwp()` [Section titled “@initBwp()”](#initbwp) Initialize a Password Manager / Vaultwarden instance accessed via the `bw` CLI. With no arguments, varlock unlocks the vault interactively on first use and caches the session token. **Key/value args (all optional):** * `sessionToken`: a pre-obtained CLI session token (e.g. `$BWP_SESSION`). When provided and non-empty, it’s used as-is instead of auto-unlocking. * `masterPassword`: master password used to unlock the vault non-interactively (e.g. in CI). Reference a `@sensitive` config item. * `sessionTtl`: how long the auto-unlocked session token is cached before a fresh `bw unlock` (default `forever`, kept until the CLI session is invalidated externally; e.g. `"1h"` to force periodic master-password re-auth). * `cacheTtl`: cache resolved item values for the specified duration. See the [Caching guide](/guides/caching/). * `appDataDir`: bw CLI data directory (`BITWARDENCLI_APPDATA_DIR`) for this instance. Point separate instances at separate dirs to read from different accounts/servers. A leading `~` is expanded. The dir must be set up independently (`bw config server` + `bw login`). * `id`: instance identifier for multiple instances. ```env-spec # @initBwp(sessionTtl="1h") # --- DB_PASSWORD=bwp("Production DB") ``` ### Data types [Section titled “Data types”](#data-types) #### `bitwardenAccessToken` [Section titled “bitwardenAccessToken”](#bitwardenaccesstoken) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents a Bitwarden Secrets Manager machine account access token. Validation ensures the token is in the correct format (`0..:`). Note that the type itself is marked as `@sensitive`, so adding an explicit `@sensitive` decorator is optional. ```env-spec # @type=bitwardenAccessToken BITWARDEN_ACCESS_TOKEN= ``` #### `bitwardenSecretId` [Section titled “bitwardenSecretId”](#bitwardensecretid) Represents a secret UUID in Bitwarden Secrets Manager. Validation ensures the ID is a valid UUID format. ```env-spec # @type=bitwardenSecretId MY_SECRET_ID=12345678-1234-1234-1234-123456789abc ``` #### `bitwardenOrganizationId` [Section titled “bitwardenOrganizationId”](#bitwardenorganizationid) Represents an organization UUID in Bitwarden. Validation ensures the ID is a valid UUID format. ```env-spec # @type=bitwardenOrganizationId BITWARDEN_ORG_ID=87654321-4321-4321-4321-cba987654321 ``` #### `bwSessionToken` [Section titled “bwSessionToken”](#bwsessiontoken) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents a Bitwarden CLI session token (the output of `bw unlock`). Only needed when supplying a token manually for non-interactive use. Marked `@sensitive`. ```env-spec # @type=bwSessionToken BWP_SESSION= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `bitwarden()` [Section titled “bitwarden()”](#bitwarden) Fetch a secret from Bitwarden Secrets Manager by UUID. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretId` (required): secret UUID in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` ```env-spec # Fetch by secret UUID DATABASE_URL=bitwarden("12345678-1234-1234-1234-123456789abc") # With instance ID PROD_SECRET=bitwarden(prod, "11111111-1111-1111-1111-111111111111") ``` #### `bwp()` [Section titled “bwp()”](#bwp) Fetch a field from a Password Manager / Vaultwarden vault item via the `bw` CLI. **Args:** * `instanceId` (optional, positional): instance identifier when multiple `@initBwp()` instances are initialized * `item` (required, positional): item name or UUID to look up * `field` (optional, key/value): which field to return. One of `password` (default), `username`, `notes`, `totp` (the stored TOTP secret/seed, not a generated code), `uri`, or a custom field name ```env-spec # Default field (password) DB_PASSWORD=bwp("Production DB") # Specific field DB_USER=bwp("Production DB", field=username) # With instance ID WORK_DB=bwp(work, "Work DB", field=password) ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Secret not found [Section titled “Secret not found”](#secret-not-found) * Verify the secret UUID is correct (must be valid UUID format) * Check that the secret exists in your Bitwarden Secrets Manager * Ensure your machine account has access to the secret or its project ### Permission denied [Section titled “Permission denied”](#permission-denied) * Verify your machine account has “Can read” or “Can read, write” permissions * Check that the machine account has access to the specific secret * Review the access settings in Bitwarden Secrets Manager console ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * Verify the access token is correct * Check if the access token has been revoked or expired * Ensure the machine account is not disabled * For self-hosted: verify `apiUrl` and `identityUrl` are correct ### Invalid UUID format [Section titled “Invalid UUID format”](#invalid-uuid-format) * Secret IDs must be valid UUIDs: `12345678-1234-1234-1234-123456789abc` * Check for typos or incorrect format * UUIDs should contain 32 hexadecimal characters and 4 hyphens ### Password Manager (`bwp()`) [Section titled “Password Manager (bwp())”](#password-manager-bwp) * **`bw` not found**: install the CLI from a trusted source and ensure it’s on your `PATH`. * **Not logged in**: run `bw login` once (for Vaultwarden, also `bw config server ` first). * **Cannot unlock without an interactive terminal**: you’re auto-unlocking (no `sessionToken` / `masterPassword` configured) in a non-TTY context. For local dev, run `varlock load` once in a regular terminal to unlock and cache the session token; for CI / headless, provide `sessionToken=$BWP_SESSION` or `masterPassword=...` to `@initBwp()`. See [Non-interactive loads](#non-interactive-loads). * **Item / field not found**: verify the item name or UUID; the error lists the item’s available custom fields. ## Resources [Section titled “Resources”](#resources) * [Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/) * [Machine Accounts Documentation](https://bitwarden.com/help/machine-accounts/) * [Self-Hosting Bitwarden](https://bitwarden.com/help/manage-your-secrets-org/#self-hosting) * [Bitwarden CLI](https://bitwarden.com/help/cli/) * [Vaultwarden](https://github.com/dani-garcia/vaultwarden) # Dashlane Plugin > Using Dashlane with Varlock [![](https://img.shields.io/npm/v/@varlock/dashlane-plugin?label=%40varlock%2Fdashlane-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/dashlane-plugin) Our [Dashlane](https://www.dashlane.com/) plugin enables secure loading of secrets from Dashlane using declarative instructions within your `.env` files. It shells out to the official Dashlane CLI (`dcli`) to resolve secret references in the `dl://` format. ## Features [Section titled “Features”](#features) * **Secret references via `dl://` URIs** (e.g. `dl:///password`) * **Headless CI/CD support** via service device keys for non-interactive authentication * **Optional vault sync** before reads with `autoSync` * **Automatic vault locking** on process exit in headless mode (configurable via `lockOnExit`) * **In-session caching** per resolution run * **Multiple instances** for accessing different Dashlane accounts * **Helpful error messages** with resolution tips ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/dashlane-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. ### Prerequisites [Section titled “Prerequisites”](#prerequisites) You must have `dcli` (Dashlane CLI) installed and available in your `PATH`. See the [Dashlane CLI installation docs](https://cli.dashlane.com/installation) for setup instructions. Note The plugin does **not** fail at load time if `dcli` is not installed. It only fails when you actually try to access a secret, making it safe to include in shared configs where some developers may not have dcli set up. ### Authentication [Section titled “Authentication”](#authentication) The plugin supports two authentication modes: **Interactive (local dev):** If you are already logged into `dcli`, just initialize without credentials: .env.schema ```env-spec # @plugin(@varlock/dashlane-plugin) # @initDashlane() ``` **Headless (CI/prod):** Use [service device keys](https://cli.dashlane.com/personal/devices) for non-interactive authentication: .env.schema ```env-spec # @plugin(@varlock/dashlane-plugin) # @initDashlane(serviceDeviceKeys=$DASHLANE_SERVICE_DEVICE_KEYS) # --- # @type(dashlaneDeviceKeys) DASHLANE_SERVICE_DEVICE_KEYS= ``` In headless mode (when `serviceDeviceKeys` is provided), the plugin automatically locks the vault when the process exits. This is best-effort and may not run in all exit scenarios (e.g. `SIGKILL`). You can control this behavior with the `lockOnExit` param. Lock your vault In interactive mode, the vault is **not** locked automatically on exit (to avoid requiring master password re-entry during live-reload). Always run `dcli lock` when you are done working. You can override this by setting `lockOnExit=true`. ### Vault sync [Section titled “Vault sync”](#vault-sync) The plugin does not sync the vault by default. To run `dcli sync` once before the first secret read, set `autoSync=true`: .env.schema ```env-spec # @plugin(@varlock/dashlane-plugin) # @initDashlane(serviceDeviceKeys=$DASHLANE_SERVICE_DEVICE_KEYS, autoSync=true) ``` Sync failures are non-blocking. If the sync fails (e.g. network issues), the plugin still reads from the existing local vault. ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) Access multiple Dashlane accounts by providing an `id`: .env.schema ```env-spec # @plugin(@varlock/dashlane-plugin) # @initDashlane(id=personal) # @initDashlane(id=team, serviceDeviceKeys=$TEAM_DASHLANE_KEYS) # --- MY_TOKEN=dashlane(personal, "dl://abc123/password") SHARED_KEY=dashlane(team, "dl://def456/password") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Use the `dashlane()` resolver function to fetch a secret by its `dl://` reference: .env.schema ```env-spec # @plugin(@varlock/dashlane-plugin) # @initDashlane() # --- DB_PASSWORD=dashlane("dl://abc123/password") API_KEY=dashlane("dl://def456/password") ``` Tip Use ID-based references (`dl:///field`) for fast, reliable lookups. Title-based references require full vault decryption and are slower. When you have multiple plugin instances, pass the instance id as the first argument: .env.schema ```env-spec DB_PASSWORD=dashlane(prod, "dl://abc123/password") ``` ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initDashlane()` [Section titled “@initDashlane()”](#initdashlane) Initialize a Dashlane plugin instance for `dashlane()` resolver. **Key/value args:** * `id` (optional): instance identifier for multiple instances * `serviceDeviceKeys` (optional): service device keys for headless authentication * `autoSync` (optional): if `true`, runs `dcli sync` once before the first read * `lockOnExit` (optional): lock the vault on process exit. Defaults to `true` in headless mode, `false` in interactive mode. ```env-spec # Interactive (local dev) # @initDashlane() # Headless (CI/prod) with auto-sync # @initDashlane(serviceDeviceKeys=$DASHLANE_SERVICE_DEVICE_KEYS, autoSync=true) # Named instance # @initDashlane(id=prod, serviceDeviceKeys=$DASHLANE_SERVICE_DEVICE_KEYS) ``` ### Data types [Section titled “Data types”](#data-types) #### `dashlaneDeviceKeys` [Section titled “dashlaneDeviceKeys”](#dashlanedevicekeys) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Service device keys for non-interactive Dashlane CLI authentication. Must start with `dls_`. See: [Dashlane CLI device registration](https://cli.dashlane.com/personal/devices) ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `dashlane()` [Section titled “dashlane()”](#dashlane) Fetch a secret from Dashlane by `dl://` reference. **Array args:** * `instanceId` (optional, if 2 args): instance identifier * `dlReference` (required): `dl://` secret reference URI ```env-spec # Default instance DB_PASSWORD=dashlane("dl://abc123/password") # With explicit instance DB_PASSWORD=dashlane(prod, "dl://abc123/password") ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `dcli` command not found [Section titled “dcli command not found”](#dcli-command-not-found) * Install dcli following the [installation docs](https://cli.dashlane.com/installation) * Ensure `dcli` is in your `PATH` ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * Verify you are logged in: `dcli sync` * For headless auth, check that `DASHLANE_SERVICE_DEVICE_KEYS` is set correctly * See [Dashlane CLI authentication](https://cli.dashlane.com/personal/authentication) for details ### Entry not found [Section titled “Entry not found”](#entry-not-found) * Verify the entry exists: `dcli password -o json | jq '.[].title'` * Use the entry ID for reliable lookups: `dashlane("dl:///password")` ### Vault locked or not synced [Section titled “Vault locked or not synced”](#vault-locked-or-not-synced) * Run `dcli sync` to sync your vault * Set `autoSync=true` in `@initDashlane` to sync automatically before reads ## Resources [Section titled “Resources”](#resources) * [Dashlane CLI documentation](https://cli.dashlane.com/) * [Dashlane CLI device registration](https://cli.dashlane.com/personal/devices) * [Dashlane CLI secret references](https://cli.dashlane.com/personal/secrets/read) # Doppler Plugin > Using Doppler with Varlock [![](https://img.shields.io/npm/v/@varlock/doppler-plugin?label=%40varlock%2Fdoppler-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/doppler-plugin) Our [Doppler](https://www.doppler.com/) plugin enables secure loading of secrets from Doppler using declarative instructions within your `.env` files. The plugin uses service tokens for programmatic access to your Doppler secrets, making it suitable for both local development and production environments. ## Features [Section titled “Features”](#features) * **Fetch secrets** from Doppler projects and configs * **Bulk-load secrets** with `dopplerBulk()` via `@setValuesBulk` * **Service token authentication** for secure, scoped API access * **Efficient caching**: a single API call is shared across all secret lookups in the same config * **Multiple plugin instances** for different projects/configs * **Auto-infer secret names** from variable names for convenience * **Helpful error messages** with resolution tips ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/doppler-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/doppler-plugin) # # 2. Initialize the plugin - see below for more details on options # @initDoppler( # project=my-project, # config=dev, # serviceToken=$DOPPLER_TOKEN # ) # --- # 3. Add your service token # @type=dopplerServiceToken @sensitive @internal DOPPLER_TOKEN= ``` ### Service token setup [Section titled “Service token setup”](#service-token-setup) 1. **Navigate to your Doppler project config** Go to the [Doppler dashboard](https://dashboard.doppler.com), select your project, and open the config (e.g., `dev`, `stg`, `prd`) you want to access. 2. **Generate a service token** Click on **Access** → **Service Tokens** → **Generate Service Token**. Give it a descriptive name. 3. **Save the token** (displayed only once!) Copy the service token immediately. It will only be displayed once. Save token securely Store the service token securely. You won’t be able to see it again after this step! 4. **Wire up the token in your config** .env.schema ```env-spec # @plugin(@varlock/doppler-plugin) # @initDoppler( # project=my-project, # config=dev, # serviceToken=$DOPPLER_TOKEN # ) # --- # @type=dopplerServiceToken @sensitive @internal DOPPLER_TOKEN= ``` 5. **Set the token in your environment** Use your CI/CD system or platform’s env var management to securely inject the `DOPPLER_TOKEN` value. For detailed instructions, see [Doppler Service Tokens documentation](https://docs.doppler.com/docs/service-tokens). ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to multiple projects or configs, register multiple named instances: .env.schema ```env-spec # @initDoppler(id=dev, project=my-app, config=dev, serviceToken=$DEV_DOPPLER_TOKEN) # @initDoppler(id=prod, project=my-app, config=prd, serviceToken=$PROD_DOPPLER_TOKEN) # --- DEV_DATABASE=doppler(dev, "DATABASE_URL") PROD_DATABASE=doppler(prod, "DATABASE_URL") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `doppler()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) Fetch secrets from Doppler: .env.schema ```env-spec # Secret name defaults to the config item key DATABASE_URL=doppler() API_KEY=doppler() # Or explicitly specify the secret name STRIPE_SECRET=doppler("STRIPE_SECRET_KEY") ``` When called without arguments, `doppler()` automatically uses the config item key as the secret name in Doppler. This provides a convenient convention-over-configuration approach. ### Using a named instance [Section titled “Using a named instance”](#using-a-named-instance) .env.schema ```env-spec # @initDoppler(id=backend, project=backend-app, config=dev, serviceToken=$BACKEND_TOKEN) # --- DB_HOST=doppler(backend, "DB_HOST") DB_PASSWORD=doppler(backend, "DB_PASSWORD") ``` ### Bulk loading secrets [Section titled “Bulk loading secrets”](#bulk-loading-secrets) Use `dopplerBulk()` with `@setValuesBulk` to load all secrets from a Doppler config at once, instead of wiring up each secret individually: .env.schema ```env-spec # @plugin(@varlock/doppler-plugin) # @initDoppler(project=my-project, config=dev, serviceToken=$DOPPLER_TOKEN) # @setValuesBulk(dopplerBulk()) # --- # @type=dopplerServiceToken @sensitive @internal DOPPLER_TOKEN= API_KEY= DB_PASSWORD= REDIS_URL= ``` With a named instance: .env.schema ```env-spec # @setValuesBulk(dopplerBulk(prod)) ``` *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initDoppler()` [Section titled “@initDoppler()”](#initdoppler) Initialize a Doppler plugin instance for accessing secrets. **Key/value args:** * `project` (required): Doppler project name * `config` (required): Config name (e.g., `dev`, `stg`, `prd`, or a branch config) * `serviceToken` (required): Doppler service token. Should be a reference to a config item of type `dopplerServiceToken`. * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). * `id` (optional): Instance identifier for multiple instances ```env-spec # @initDoppler( # project=my-project, # config=dev, # serviceToken=$DOPPLER_TOKEN, # cacheTtl="1h" # ) # --- # @type=dopplerServiceToken @sensitive @internal DOPPLER_TOKEN= ``` ### Data types [Section titled “Data types”](#data-types) #### `dopplerServiceToken` [Section titled “dopplerServiceToken”](#dopplerservicetoken) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents a Doppler service token. This type is marked as `@sensitive`. ```env-spec # @type=dopplerServiceToken @sensitive @internal DOPPLER_TOKEN= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `doppler()` [Section titled “doppler()”](#doppler) Fetch a secret from Doppler. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretName` (optional): secret name in Doppler. If omitted, uses the variable name. ```env-spec # Auto-infer secret name from variable DATABASE_URL=doppler() # Explicit secret name STRIPE_KEY=doppler("STRIPE_SECRET_KEY") # With instance ID DEV_SECRET=doppler(dev, "DATABASE_URL") ``` #### `dopplerBulk()` [Section titled “dopplerBulk()”](#dopplerbulk) Bulk-load all secrets from a Doppler config. Intended for use with `@setValuesBulk`. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized ```env-spec # Load all secrets from default instance # @setValuesBulk(dopplerBulk()) # With instance ID # @setValuesBulk(dopplerBulk(prod)) ``` *** ## Example Configurations [Section titled “Example Configurations”](#example-configurations) ### Development setup with auto-named secrets [Section titled “Development setup with auto-named secrets”](#development-setup-with-auto-named-secrets) .env.schema ```env-spec # @plugin(@varlock/doppler-plugin) # @initDoppler(project=my-app, config=dev, serviceToken=$DOPPLER_TOKEN) # --- # @type=dopplerServiceToken @sensitive @internal DOPPLER_TOKEN= # Secret names automatically match config keys DATABASE_URL=doppler() REDIS_URL=doppler() STRIPE_KEY=doppler() ``` ### Multi-environment setup [Section titled “Multi-environment setup”](#multi-environment-setup) .env.schema ```env-spec # @plugin(@varlock/doppler-plugin) # @initDoppler(id=dev, project=my-app, config=dev, serviceToken=$DEV_DOPPLER_TOKEN) # @initDoppler(id=staging, project=my-app, config=stg, serviceToken=$STG_DOPPLER_TOKEN) # @initDoppler(id=prod, project=my-app, config=prd, serviceToken=$PROD_DOPPLER_TOKEN) # --- DEV_DATABASE=doppler(dev, "DATABASE_URL") STAGING_DATABASE=doppler(staging, "DATABASE_URL") PROD_DATABASE=doppler(prod, "DATABASE_URL") ``` ### Bulk loading for simple setups [Section titled “Bulk loading for simple setups”](#bulk-loading-for-simple-setups) .env.schema ```env-spec # @plugin(@varlock/doppler-plugin) # @initDoppler(project=my-app, config=dev, serviceToken=$DOPPLER_TOKEN) # @setValuesBulk(dopplerBulk()) # --- # @type=dopplerServiceToken @sensitive @internal DOPPLER_TOKEN= # These will be populated from Doppler secrets with matching names DATABASE_URL= API_KEY= STRIPE_SECRET_KEY= SENDGRID_API_KEY= ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Secret not found [Section titled “Secret not found”](#secret-not-found) * Verify the secret exists in your Doppler project config * Check the secret name matches exactly (case-sensitive) * Ensure you’re looking at the correct config (dev vs stg vs prd) ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * Verify the service token is correct and not expired * Generate a new service token from the Doppler dashboard * Check that the service token has access to the requested project/config ### Access denied [Section titled “Access denied”](#access-denied) * Service tokens are scoped to a specific config, so ensure you’re using the right one * Verify the token hasn’t been revoked ### Wrong config [Section titled “Wrong config”](#wrong-config) * Double-check the `config` parameter matches the Doppler config where your secrets are stored * Remember Doppler configs are hierarchical (root → development/staging/production → branch configs) ## Resources [Section titled “Resources”](#resources) * [Doppler Documentation](https://docs.doppler.com) * [Service Tokens](https://docs.doppler.com/docs/service-tokens) * [Doppler API Reference](https://docs.doppler.com/reference) * [Projects and Configs](https://docs.doppler.com/docs/enclave-project-setup) # Google Secret Manager Plugin > Using Google Cloud Secret Manager with Varlock [![](https://img.shields.io/npm/v/@varlock/google-secret-manager-plugin?label=%40varlock%2Fgoogle-secret-manager-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/google-secret-manager-plugin) Our [Google Cloud Secret Manager](https://cloud.google.com/secret-manager) plugin enables secure loading of secrets from GCP Secret Manager using declarative instructions within your `.env` files. It supports authentication via [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials), explicitly passing in a service account JSON key, or [OIDC Workload Identity Federation](/guides/oidc/). **Key features:** * ✅ Automatic secret naming using config item keys * ✅ Application Default Credentials or Service Account authentication * ✅ [OIDC Workload Identity Federation](/guides/oidc/) for platform-based authentication (Vercel, GitHub Actions, etc.) * ✅ Versioned secret access * ✅ Multiple plugin instances for different projects ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/google-secret-manager-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/google-secret-manager-plugin) # # 2. Initialize the plugin - see below for more details on options # @initGsm(projectId=my-gcp-project) ``` Project ID The `projectId` parameter is optional if your service account JSON includes a `project_id` field, or if you have set `GOOGLE_CLOUD_PROJECT` in your environment. However, it’s recommended to set it explicitly for clarity. If you don’t provide `projectId` and are using ADC, the plugin will attempt to detect it from your `gcloud` configuration or environment variables. ### Using Application Default Credentials (ADC) - recommended [Section titled “Using Application Default Credentials (ADC) - recommended”](#using-application-default-credentials-adc---recommended) By default (when no `credentials` parameter is set), this plugin will use [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials) to authenticate with Google Cloud. This is the recommended way to authenticate for local dev, and within GCP. Within GCP, you will need to set up a [service account](https://cloud.google.com/iam/docs/service-accounts) with the correct permissions, and attach it to the resources where your code will be running. Outside of GCP, you may set up ADC credentials using the [`gcloud auth application-default login`](https://docs.cloud.google.com/sdk/gcloud/reference/auth/application-default/login) command, which will store credentials locally, and make them available for ADC. ### Using a service account key [Section titled “Using a service account key”](#using-a-service-account-key) In rare cases, it may be useful to pass in a service account key explicitly. This is useful for deployed environments other than GCP, or if you need to use a different service account than the one attached to your service. 1. **Create and download a JSON key** for your service account. This can be done via the [Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts) or using the `gcloud` CLI. [docs](https://cloud.google.com/iam/docs/keys-create-delete) ```bash gcloud iam service-accounts keys create key.json \ --iam-account=SERVICE_ACCOUNT_EMAIL ``` 2. **Wire up the service account key in your config**. Add a config item of type `gcpServiceAccountJson` to hold the key value, and reference it when initializing the plugin. .env.schema ```diff # @plugin(@varlock/google-secret-manager-plugin) # @initGsm(projectId=my-gcp-project, credentials=$GCP_SA_KEY) # --- +# @type=gcpServiceAccountJson @sensitive @internal GCP_SA_KEY= ``` 3. **Set your service account key in deployed environments**. Copy the JSON key content from the file you downloaded, and set it in deployed environments using your platform’s env var management UI. Be sure to use the same name as you defined in your schema (e.g. `GCP_SA_KEY`). Keep service account keys secure Service account JSON keys are highly sensitive credentials. Store them securely and never commit them to version control. Consider using your platform’s secret management system to store the key itself. ### OIDC Workload Identity Federation [Section titled “OIDC Workload Identity Federation”](#oidc-workload-identity-federation) For deployments on platforms that issue OIDC tokens (Vercel, GitHub Actions, GitLab CI, Fly.io), you can use [OIDC workload identity federation](/guides/oidc/) to authenticate without a service account key. Varlock auto-detects the platform’s OIDC token and exchanges it for GCP credentials via Workload Identity Federation. **Provider-side setup:** 1. Create a Workload Identity Pool and Provider: ```bash gcloud iam workload-identity-pools create varlock-pool \ --location=global gcloud iam workload-identity-pools providers create-oidc vercel \ --location=global \ --workload-identity-pool=varlock-pool \ --issuer-uri=https://oidc.vercel.com \ --attribute-mapping="google.subject=assertion.sub" ``` 2. Grant the pool access to a service account: ```bash gcloud iam service-accounts add-iam-policy-binding \ my-sa@my-project.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/varlock-pool/*" ``` 3. Grant the service account Secret Manager access. **Varlock config:** .env.schema ```env-spec # @plugin(@varlock/google-secret-manager-plugin) # @initGsm( # projectId=my-project, # workloadIdentityProvider="//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/varlock-pool/providers/vercel", # serviceAccountEmail="my-sa@my-project.iam.gserviceaccount.com" # ) ``` The `workloadIdentityProvider` is the full resource name of the provider. For platforms not auto-detected, pass an explicit token via `oidcToken`. ### GCP Prerequisites [Section titled “GCP Prerequisites”](#gcp-prerequisites) If you are already using GCP Secret Manager, you likely have completed these steps already, but if not, you will need to do so before using this plugin: 1. **Enable the Secret Manager API** (if not already done) Go to the Google Cloud Console and enable the Secret Manager API for your project. 2. **Create a new service account** in your GCP project. This can be done via the [Google Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts) or using the `gcloud` CLI. [docs](https://cloud.google.com/iam/docs/service-accounts-create) This service account, whether accessed using ADC or a service account key, will now serve as your *secret-zero*, granting access to the rest of your sensitive data stored in Secret Manager. For local-only workflows, you can use [device-local encryption](/guides/local-encryption/) to secure this value. 3. **Grant the service account permissions** to access secrets. The service account needs the `Secret Manager Secret Accessor` role (`roles/secretmanager.secretAccessor`) on the secrets or project level. [docs](https://cloud.google.com/secret-manager/docs/access-control) ```bash gcloud projects add-iam-policy-binding PROJECT_ID \ --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \ --role="roles/secretmanager.secretAccessor" ``` 4. **Attach the service account to GCP resources** You must [attach this service account](https://docs.cloud.google.com/iam/docs/attach-service-accounts#attaching-to-resources) to any resources where your code will be running. ## Pulling data from Secret Manager [Section titled “Pulling data from Secret Manager”](#pulling-data-from-secret-manager) Once the plugin is installed and initialized, you can start adding config items that load values from Google Secret Manager using the new `gsm()` resolver function. ### Basic Secret Fetching [Section titled “Basic Secret Fetching”](#basic-secret-fetching) ```env-spec # @plugin(@varlock/google-secret-manager-plugin) # @initGsm(projectId=my-project) # --- # Secret name defaults to the config item key SIMPLEST_VAR=gsm() # Or you can explicitly specify the secret name RENAMED_VAR=gsm("database-password") # You can fetch a specific version API_KEY_LATEST=gsm("api-key@latest") API_KEY_V5=gsm("api-key@5") # Use complete resource paths for maximum control: FULL_PATH_VAR=gsm("projects/my-project/secrets/db-url/versions/3") ``` Auto-naming When called without arguments, `gsm()` automatically uses the config item key as the secret name in Google Secret Manager (e.g., `DATABASE_URL=gsm()` will fetch a secret named `DATABASE_URL`). This provides a clean, convention-over-configuration approach when your secret names match your config keys. Secret versions If you don’t specify a version in the short format, `latest` will be used automatically. For the full path format, you must include the version (use `latest` if you want the most recent version). ### Multiple plugin instances [Section titled “Multiple plugin instances”](#multiple-plugin-instances) If you need to connect using different project ids, or different credentials, particularly at the same time, you can create multiple named instances, and then use that id when fetching secrets. ```env-spec # @plugin(@varlock/google-secret-manager-plugin) # @initGsm(id=prod, projectId=prod-project, credentials=$PROD_KEY) # @initGsm(id=dev, projectId=dev-project, credentials=$DEV_KEY) # --- PROD_DATABASE=gsm(prod, "database-url") DEV_DATABASE=gsm(dev, "database-url") ``` ### Dynamic project ID [Section titled “Dynamic project ID”](#dynamic-project-id) The `projectId` parameter supports dynamic resolution, allowing you to specify project IDs from environment variables or other resolver functions. This is useful when you need to use different project IDs based on your deployment environment. ```env-spec # @plugin(@varlock/google-secret-manager-plugin) # @initGsm(projectId=$GCP_PROJECT_ID) # --- # Use environment variable for project ID GCP_PROJECT_ID= API_KEY=gsm("api-key") ``` You can also use resolver functions to construct the project ID dynamically: ```env-spec # @plugin(@varlock/google-secret-manager-plugin) # @initGsm(projectId=concat($APP_ENV, "-project")) # --- APP_ENV= API_KEY=gsm("api-key") ``` *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initGsm()` [Section titled “@initGsm()”](#initgsm) Initializes an instance of the Google Secret Manager plugin - setting up options and authentication. Can be called multiple times to set up different instances. **Key/value args:** * `id` (optional): identifier for this instance, used when multiple instances are needed * `projectId` (optional): GCP project ID. Required for short secret reference format, or if credentials don’t include a `project_id` field * `credentials` (optional): service account JSON key. Should be a reference to a config item of type `gcpServiceAccountJson`. If omitted, Application Default Credentials will be used * `workloadIdentityProvider` (optional): full resource name of the Workload Identity Provider for OIDC authentication * `serviceAccountEmail` (optional): service account email to impersonate via Workload Identity Federation * `oidcToken` (optional): explicit OIDC JWT token (auto-detected from platform if omitted) * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). ```env-spec # @initGsm(id=prod, projectId=my-gcp-project, credentials=$GCP_SA_KEY) # --- # @type=gcpServiceAccountJson @sensitive @internal GCP_SA_KEY= ``` ### Data types [Section titled “Data types”](#data-types) #### `gcpServiceAccountJson` [Section titled “gcpServiceAccountJson”](#gcpserviceaccountjson) Represents a [Google Cloud service account JSON key](https://cloud.google.com/iam/docs/service-accounts). Validation ensures the JSON is valid and contains required fields (`type`, `project_id`, `private_key`, `client_email`). The type itself is marked as `@sensitive`, so adding an explicit `@sensitive` decorator is optional. ```env-spec # @type=gcpServiceAccountJson GCP_SA_KEY= ``` Does your app use these credentials too? The examples here mark the credential [`@internal`](/reference/item-decorators/#internal) so it’s used by varlock to fetch your secrets but **not** injected into your app. This is the recommended posture when varlock is the only consumer. Unlike the dedicated secrets-manager tokens, GCP service account keys are **not** `@internal` by default (they’re commonly read directly by the Google Cloud SDK), so if you need the credential at runtime for other purposes, opt out with `@internal=false` to keep it injected: ```env-spec # @type=gcpServiceAccountJson @internal=false GCP_SA_KEY= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `gsm()` [Section titled “gsm()”](#gsm) Fetches a secret value from Google Secret Manager **Signatures:** * `gsm()` - Fetch using config item key as secret name from default instance (e.g., `DATABASE_URL=gsm()` will fetch a secret named `DATABASE_URL`) * `gsm(secretRef)` - Fetch specific secret from default instance * `gsm(instanceId, secretRef)` - Fetch from named instance **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretReference` (optional): secret reference, either in short format (`"secret-name"` or `"secret-name@version"`) or full path format (`"projects/PROJECT_ID/secrets/SECRET_NAME/versions/VERSION"`). If omitted, uses the config item key as the secret name **Secret Reference Formats:** * `"secret-name"` - Uses latest version from configured project * `"secret-name@5"` - Specific version from configured project * `"projects/PROJECT/secrets/NAME/versions/VERSION"` - Full resource path **Returns:** The secret value as a string ```env-spec # Secret name defaults to the config item key SIMPLEST_VAR=gsm() # Or you can explicitly specify the secret name RENAMED_VAR=gsm("database-password") # You can fetch a specific version API_KEY_V5=gsm("api-key@5") # Use complete resource paths for maximum control: FULL_PATH_VAR=gsm("projects/my-project/secrets/db-url/versions/3") # Example using a plugin instance id PROD_SECRET=gsm(prod, "prod-secret") ``` # HashiCorp Vault Plugin > Using HashiCorp Vault and OpenBao with Varlock [![](https://img.shields.io/npm/v/@varlock/hashicorp-vault-plugin?label=%40varlock%2Fhashicorp-vault-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/hashicorp-vault-plugin) Our HashiCorp Vault plugin enables secure loading of secrets from [HashiCorp Vault](https://www.vaultproject.io/) (KV v2 secrets engine) and [OpenBao](https://openbao.org/) using declarative instructions within your `.env` files. The plugin supports multiple authentication methods including explicit tokens, AppRole for CI/CD, JWT/OIDC for platform-based workload identity, and automatic CLI token detection for local development. ## Features [Section titled “Features”](#features) * **Zero-config authentication** - Automatically uses Vault token from CLI login * **AppRole authentication** - For automated and CI/CD workflows * **Vault CLI integration** - Works with `vault login` for local development * **OpenBao compatible** - Works with OpenBao (detects `~/.bao-token` automatically) * **Auto-infer secret keys** from environment variable names * **JSON key extraction** from secrets using `#` syntax or named `key` parameter * **Path prefixing** with `pathPrefix` option for organized secret management * **Default path** support for sharing a common secret path across items * **[JWT/OIDC authentication](/guides/oidc/)** - Authenticate using platform OIDC tokens (Vercel, GitHub Actions, etc.) * Support for Vault Enterprise namespaces * Support for multiple Vault instances ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/hashicorp-vault-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/hashicorp-vault-plugin) # # 2. Initialize the plugin - see below for more details on options # @initHcpVault(url="https://vault.example.com:8200") ``` ### Authentication options [Section titled “Authentication options”](#authentication-options) The plugin tries authentication methods in this priority order: 1. **Explicit token** - If `token` is provided in `@initHcpVault()` 2. **JWT/OIDC** - If `jwtRole` is provided, uses [OIDC workload identity federation](/guides/oidc/) 3. **AppRole** - If both `roleId` and `secretId` are provided 4. **CLI token file** - From `~/.vault-token` (created by `vault login`) or `~/.bao-token` (created by `bao login` for OpenBao) ### Automatic authentication (Recommended for local dev) [Section titled “Automatic authentication (Recommended for local dev)”](#automatic-authentication-recommended-for-local-dev) For local development, just provide the Vault URL - the plugin will pick up your CLI token automatically: .env.schema ```env-spec # @plugin(@varlock/hashicorp-vault-plugin) # @initHcpVault(url="https://vault.example.com:8200") ``` **How this works:** * **Local development:** Run `vault login` → automatically uses the token from `~/.vault-token` * **OpenBao users:** Run `bao login` → automatically uses the token from `~/.bao-token` ### AppRole auth (For CI/CD and automated workflows) [Section titled “AppRole auth (For CI/CD and automated workflows)”](#approle-auth-for-cicd-and-automated-workflows) AppRole is the recommended auth method for CI/CD and server environments: 1. **Set up AppRole in Vault** (see Vault Setup section below) 2. **Wire up the credentials in your config**. Add config items for the role ID and secret ID, and reference them when initializing the plugin. .env.schema ```env-spec # @plugin(@varlock/hashicorp-vault-plugin) # @initHcpVault( # url="https://vault.example.com:8200", # roleId=$VAULT_ROLE_ID, # secretId=$VAULT_SECRET_ID # ) # --- VAULT_ROLE_ID= # @sensitive VAULT_SECRET_ID= ``` 3. **Set your credentials in deployed environments**. Use your platform’s env var management UI to securely inject these values. ### JWT/OIDC authentication [Section titled “JWT/OIDC authentication”](#jwtoidc-authentication) For deployments on platforms that issue OIDC tokens (Vercel, GitHub Actions, GitLab CI, Fly.io, GCP), you can use [OIDC workload identity federation](/guides/oidc/) to authenticate with Vault’s JWT/OIDC auth method. Varlock auto-detects the platform’s OIDC token and uses it to authenticate. **Provider-side setup:** 1. Enable the JWT auth method: ```bash vault auth enable jwt ``` 2. Configure it with the OIDC provider: ```bash vault write auth/jwt/config \ oidc_discovery_url="https://oidc.vercel.com" ``` 3. Create a role with bound claims: ```bash vault write auth/jwt/role/varlock-role \ role_type="jwt" \ bound_audiences="https://vault.example.com" \ user_claim="sub" \ policies="secrets-reader" \ ttl=1h ``` **Varlock config:** .env.schema ```env-spec # @plugin(@varlock/hashicorp-vault-plugin) # @initHcpVault(url="https://vault.example.com:8200", jwtRole="varlock-role") ``` You can optionally set `jwtAuthPath` if the JWT auth method is mounted at a non-default path. For platforms not auto-detected, pass an explicit token via `oidcToken`. ### Explicit token [Section titled “Explicit token”](#explicit-token) You can also provide a token directly: .env.schema ```env-spec # @initHcpVault( # url="https://vault.example.com:8200", # token=$VAULT_TOKEN # ) # --- # @type=vaultToken @sensitive @internal VAULT_TOKEN= ``` ### Vault Enterprise namespaces [Section titled “Vault Enterprise namespaces”](#vault-enterprise-namespaces) For Vault Enterprise, specify the namespace: .env.schema ```env-spec # @initHcpVault(url="https://vault.example.com:8200", namespace="admin/team-a") ``` ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to multiple Vault instances, register named instances: .env.schema ```env-spec # @initHcpVault(id=prod, url="https://vault-prod.example.com:8200") # @initHcpVault(id=dev, url="https://vault-dev.example.com:8200") # --- PROD_KEY=vaultSecret(prod, "secret/api/keys#API_KEY") DEV_KEY=vaultSecret(dev, "secret/api/keys#API_KEY") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `vaultSecret()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) Since Vault KV v2 always stores key/value pairs, the item key (variable name) is automatically used as the JSON key to extract from the secret: .env.schema ```env-spec # Fetches "secret/db/config" and extracts "DB_HOST" key DB_HOST=vaultSecret("secret/db/config") # Override the extracted key with # syntax DB_PASSWORD=vaultSecret("secret/db/config#password") # Or use named "key" parameter DB_PORT=vaultSecret("secret/db/config", key="PORT") # Fetch entire secret as JSON blob DB_CONFIG=vaultSecret("secret/db/config", raw=true) ``` ### Default path [Section titled “Default path”](#default-path) Use `defaultPath` to set a common path for secrets when no path argument is provided: .env.schema ```env-spec # @initHcpVault(url="https://vault.example.com:8200", defaultPath=secret/myapp/config) # --- # Both fetch from "secret/myapp/config" extracting item key DB_PASSWORD=vaultSecret() API_KEY=vaultSecret() # Override the inferred key using # syntax STRIPE_KEY=vaultSecret("#stripe_api_key") # Explicit path still extracts item key by default OTHER_SECRET=vaultSecret("secret/other/path") ``` ### Path prefixing [Section titled “Path prefixing”](#path-prefixing) Use `pathPrefix` to automatically prefix all secret paths for better organization: .env.schema ```env-spec # @initHcpVault(url="https://vault.example.com:8200", pathPrefix="secret/myapp") # --- # Fetches from "secret/myapp/db/config" DB_HOST=vaultSecret("db/config#HOST") ``` You can even use dynamic prefixes: .env.schema ```env-spec # @initHcpVault(url="https://vault.example.com:8200", pathPrefix="secret/${ENV}") # --- # In prod: fetches from "secret/prod/db/config" # In dev: fetches from "secret/dev/db/config" DB_HOST=vaultSecret("db/config#HOST") ``` ### Bulk loading secrets [Section titled “Bulk loading secrets”](#bulk-loading-secrets) Use `raw=true` with `@setValuesBulk` to load all key/value pairs from a Vault path at once, instead of wiring up each secret individually: .env.schema ```env-spec # @initHcpVault(url="https://vault.example.com:8200") # @setValuesBulk(vaultSecret("secret/myapp/config", raw=true)) # --- DB_HOST= DB_PASSWORD= API_KEY= ``` This fetches all keys stored at `secret/myapp/config` and maps them to matching item keys. Only items declared in your schema will be populated; any extra keys in the Vault secret are ignored. *** ## Vault Setup [Section titled “Vault Setup”](#vault-setup) ### Enable KV v2 secrets engine [Section titled “Enable KV v2 secrets engine”](#enable-kv-v2-secrets-engine) ```bash # KV v2 is enabled by default at "secret/" in dev mode # For production, enable it explicitly: vault secrets enable -version=2 -path=secret kv ``` ### Create a policy [Section titled “Create a policy”](#create-a-policy) Create a policy that allows reading secrets: policy.hcl ```hcl path "secret/data/*" { capabilities = ["read"] } ``` ```bash vault policy write varlock-reader policy.hcl ``` Least privilege principle For production, scope down the `path` to only the specific secret paths your application needs. For example: `path "secret/data/myapp/*"` ### Set up AppRole auth (Recommended for CI/CD) [Section titled “Set up AppRole auth (Recommended for CI/CD)”](#set-up-approle-auth-recommended-for-cicd) 1. **Enable AppRole auth method** ```bash vault auth enable approle ``` 2. **Create a role** ```bash vault write auth/approle/role/varlock-role \ secret_id_ttl=24h \ token_ttl=1h \ token_max_ttl=4h \ token_policies=varlock-reader ``` 3. **Get the role ID and generate a secret ID** ```bash vault read auth/approle/role/varlock-role/role-id vault write -f auth/approle/role/varlock-role/secret-id ``` Save the `role_id` and `secret_id` from the output for your CI/CD configuration. ### Create a token (For simple setups) [Section titled “Create a token (For simple setups)”](#create-a-token-for-simple-setups) ```bash vault token create -policy=varlock-reader -ttl=24h ``` ### Store secrets [Section titled “Store secrets”](#store-secrets) ```bash # Store a single key/value vault kv put secret/myapp/config DB_PASSWORD=supersecret # Store multiple keys vault kv put secret/myapp/config \ DB_HOST=db.example.com \ DB_PASSWORD=supersecret \ API_KEY=abc123 ``` ### Vault CLI for local development [Section titled “Vault CLI for local development”](#vault-cli-for-local-development) 1. **Set the Vault address** ```bash export VAULT_ADDR="https://vault.example.com:8200" ``` 2. **Login to Vault** ```bash vault login ``` This writes a token to `~/.vault-token` which the plugin will automatically pick up. 3. **Test the configuration** ```bash vault kv get secret/myapp/config ``` *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initHcpVault()` [Section titled “@initHcpVault()”](#inithcpvault) Initialize a HashiCorp Vault / OpenBao plugin instance. **Key/value args:** * `url` (required): Vault server URL (e.g., `https://vault.example.com:8200`) * `token` (optional): Explicit Vault authentication token * `roleId` (optional): AppRole role ID for automated authentication * `secretId` (optional): AppRole secret ID for automated authentication * `namespace` (optional): Vault Enterprise namespace * `jwtRole` (optional): Vault JWT/OIDC auth role name for OIDC authentication * `jwtAuthPath` (optional): Mount path for the JWT auth method (defaults to `jwt`) * `oidcToken` (optional): Explicit OIDC JWT token (auto-detected from platform if omitted) * `defaultPath` (optional): Default secret path when no path argument is given to `vaultSecret()` * `pathPrefix` (optional): Prefix automatically prepended to all secret paths * `id` (optional): Instance identifier for multiple instances * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). ```env-spec # @initHcpVault(url="https://vault.example.com:8200", defaultPath=secret/myapp/config) ``` ### Data types [Section titled “Data types”](#data-types) #### `vaultToken` [Section titled “vaultToken”](#vaulttoken) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents a HashiCorp Vault authentication token. This type is marked as `@sensitive`. ```env-spec # @type=vaultToken VAULT_TOKEN= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `vaultSecret()` [Section titled “vaultSecret()”](#vaultsecret) Fetch a secret from HashiCorp Vault’s KV v2 secrets engine. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretRef` (optional): secret path, optionally with `#KEY` to override the extracted key. If omitted, uses `defaultPath`. Use `#key` (without a path) to override the inferred key while still using `defaultPath`. **Named args:** * `key` (optional): JSON key to extract from the secret (overrides `#KEY` syntax and item key default) * `raw` (optional): set to `true` to return all key/value pairs as a JSON blob instead of extracting a single key. Useful with `@setValuesBulk` for bulk loading. **Key extraction:** By default, the item key (variable name) is used as the JSON key. Override with `#KEY` or `key=`, or use `raw=true` to get everything. **How paths work:** Vault KV v2 stores key/value pairs at a path. Given a path like `secret/myapp/config`, the plugin calls `GET /v1/secret/data/myapp/config` (the first path segment is the mount point, and `/data/` is inserted for the KV v2 API). ```env-spec # Uses defaultPath, extracts item key DATABASE_URL=vaultSecret() # Override inferred key using # syntax (still uses defaultPath) STRIPE_KEY=vaultSecret("#stripe_api_key") # Explicit path, extracts item key "DB_HOST" DB_HOST=vaultSecret("secret/db/config") # Override key with # syntax DB_PASSWORD=vaultSecret("secret/db/config#password") # Extract key (named parameter) DB_PORT=vaultSecret("secret/db/config", key="PORT") # Fetch full secret as JSON DB_CONFIG=vaultSecret("secret/db/config", raw=true) # With instance ID PROD_SECRET=vaultSecret(prod, "secret/api/keys") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Secret not found [Section titled “Secret not found”](#secret-not-found) * Verify the secret exists: `vault kv get secret/myapp/config` * Check the mount point is correct (first path segment, typically `secret`) * Ensure you’re using KV v2, not KV v1 (different API format) ### Permission denied [Section titled “Permission denied”](#permission-denied) * Check your token’s policies: `vault token lookup` * Ensure your policy includes `read` capability on `secret/data/*` (note the `/data/` prefix for KV v2) * For AppRole: verify the role has the correct policies attached ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * **Local dev:** Run `vault login` (or `bao login` for OpenBao) and ensure `VAULT_ADDR` is set correctly * **CI/CD:** Verify your token or AppRole credentials are properly wired up in `@initHcpVault()` * Check if the token has expired: `vault token lookup` * For AppRole: verify the secret ID hasn’t expired and generate a new one if needed ### JSON key not found [Section titled “JSON key not found”](#json-key-not-found) * Verify the key exists at the path: `vault kv get -field=MY_KEY secret/myapp/config` * Key names are case-sensitive * Check available keys: `vault kv get secret/myapp/config` ## Resources [Section titled “Resources”](#resources) * [HashiCorp Vault](https://www.vaultproject.io/) * [OpenBao](https://openbao.org/) * [Vault KV v2 Secrets Engine](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2) * [Vault AppRole Auth Method](https://developer.hashicorp.com/vault/docs/auth/approle) * [Vault Tokens](https://developer.hashicorp.com/vault/docs/concepts/tokens) # Infisical Plugin > Using Infisical with Varlock [![](https://img.shields.io/npm/v/@varlock/infisical-plugin?label=%40varlock%2Finfisical-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/infisical-plugin) Our [Infisical](https://infisical.com/) plugin loads secrets from Infisical using declarative instructions within your `.env` files. The plugin uses machine identities with Universal Auth or [OIDC workload identity](/guides/oidc/) for programmatic access to your Infisical secrets, making it suitable for both CI/CD and production environments. ## Features [Section titled “Features”](#features) * **Fetch secrets** from Infisical projects and environments * **Bulk-load secrets** with `infisicalBulk()` via `@setValuesBulk` * **Universal Auth** with Client ID and Client Secret * **[OIDC authentication](/guides/oidc/)**: Authenticate using platform OIDC tokens (Vercel, GitHub Actions, etc.) * **Support for self-hosted** Infisical instances * **Secret paths** for hierarchical organization * **Filter by tag** to load only matching secrets * **Multiple plugin instances** for different projects/environments * **Auto-infer secret names** from variable names for convenience * **`allowMissing`** option for optional secrets * **Helpful error messages** with resolution tips ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/infisical-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/infisical-plugin) # # 2. Initialize the plugin - see below for more details on options # @initInfisical( # projectId=your-project-id, # environment=dev, # clientId=$INFISICAL_CLIENT_ID, # clientSecret=$INFISICAL_CLIENT_SECRET # ) # --- # 3. Add machine identity credentials # @type=infisicalClientId INFISICAL_CLIENT_ID= # @type=infisicalClientSecret @sensitive @internal INFISICAL_CLIENT_SECRET= ``` Note for EU users If you are using the Infisical EU Cloud, you must set `@initFisical(siteUrl=https://eu.infisical.com)` (See [Universal Auth Documentation](https://infisical.com/docs/documentation/platform/identities/universal-auth)) ### Machine identity setup [Section titled “Machine identity setup”](#machine-identity-setup) 1. **Create a machine identity** in Infisical Navigate to your Infisical project settings → **Access Control** → **Machine Identities** → Click **Create Identity**. 2. **Select Universal Auth** Choose **Universal Auth** as the authentication method. 3. **Save the credentials** (displayed only once!) Copy the **Client ID** and **Client Secret** immediately - they will only be displayed once. Save credentials securely Store the client ID and secret securely. You won’t be able to see them again after this step! 4. **Grant access to your project and environment** Ensure the machine identity has access to the specific project and environment you’ll be using. 5. **Wire up the credentials in your config** .env.schema ```env-spec # @plugin(@varlock/infisical-plugin) # @initInfisical( # projectId=your-project-id, # environment=dev, # clientId=$INFISICAL_CLIENT_ID, # clientSecret=$INFISICAL_CLIENT_SECRET # ) # --- # @type=infisicalClientId INFISICAL_CLIENT_ID= # @type=infisicalClientSecret @sensitive @internal INFISICAL_CLIENT_SECRET= ``` 6. **Set your credentials in environments** Use your CI/CD system or platform’s env var management to securely inject the credential values. For detailed instructions, see [Infisical Machine Identities documentation](https://infisical.com/docs/documentation/platform/identities/machine-identities). ### OIDC authentication [Section titled “OIDC authentication”](#oidc-authentication) For deployments on platforms that issue OIDC tokens (Vercel, GitHub Actions, GitLab CI, Fly.io, GCP), you can use [OIDC workload identity federation](/guides/oidc/) to authenticate without client credentials. Varlock auto-detects the platform’s OIDC token and uses it to authenticate via Infisical’s OIDC machine identity auth. **Provider-side setup:** 1. Create a Machine Identity in Infisical 2. Add an OIDC auth method to the identity, configuring the issuer and claims 3. Grant the identity access to your project **Varlock config:** .env.schema ```env-spec # @plugin(@varlock/infisical-plugin) # @initInfisical( # projectId=your-project-id, # environment=production, # identityId=your-identity-id # ) ``` No `clientId` or `clientSecret` is needed. The `identityId` parameter triggers OIDC authentication. For platforms not auto-detected, pass an explicit token via `oidcToken`. ### Self-hosted Infisical [Section titled “Self-hosted Infisical”](#self-hosted-infisical) For self-hosted Infisical instances, specify the `siteUrl`: .env.schema ```env-spec # @plugin(@varlock/infisical-plugin) # @initInfisical( # projectId=my-project, # environment=production, # clientId=$CLIENT_ID, # clientSecret=$CLIENT_SECRET, # siteUrl=https://infisical.mycompany.com # ) ``` `siteUrl` can also be set dynamically. Pass a reference like `siteUrl=$INFISICAL_SITE_URL` to point at different Infisical instances per environment. ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to multiple projects or environments, register multiple named instances: .env.schema ```env-spec # @initInfisical(id=dev, projectId=dev-project, environment=development, clientId=$DEV_CLIENT_ID, clientSecret=$DEV_CLIENT_SECRET) # @initInfisical(id=prod, projectId=prod-project, environment=production, clientId=$PROD_CLIENT_ID, clientSecret=$PROD_CLIENT_SECRET) # --- DEV_DATABASE=infisical(dev, "DATABASE_URL") PROD_DATABASE=infisical(prod, "DATABASE_URL") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `infisical()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) Fetch secrets from Infisical: .env.schema ```env-spec # Secret name defaults to the config item key DATABASE_URL=infisical() API_KEY=infisical() # Or explicitly specify the secret name STRIPE_SECRET=infisical("STRIPE_SECRET_KEY") ``` When called without arguments, `infisical()` uses the config item key as the secret name in Infisical. ### Optional secrets [Section titled “Optional secrets”](#optional-secrets) Use `allowMissing=true` on `infisical()` to return `undefined` when a secret doesn’t exist in Infisical, then combine with `fallback()` to provide a default value: .env.schema ```env-spec RESEND_API_KEY=fallback(infisical(allowMissing=true), undefined) SOME_OPTIONAL_KEY=fallback(infisical(allowMissing=true), "") ``` You can also set `allowMissing` on `@initInfisical()` to apply it to all `infisical()` calls for that instance. It can be a dynamic value, e.g. `forEnv(development)` to only allow missing secrets in dev: .env.schema ```env-spec # @initInfisical(projectId=my-project, environment=dev, clientId=$ID, clientSecret=$SECRET, allowMissing=forEnv(development)) ``` When `allowMissing` is enabled, mark optional items with `@required=false` or wrap the resolver with `fallback()` so validation does not fail. ### Using secret paths [Section titled “Using secret paths”](#using-secret-paths) Organize secrets with hierarchical paths: .env.schema ```env-spec # Default path for all secrets # @initInfisical(projectId=my-project, environment=production, clientId=$ID, clientSecret=$SECRET, secretPath=/production/app) # --- # Fetches from /production/app/DB_PASSWORD DB_PASSWORD=infisical("DB_PASSWORD") ``` Or specify path per secret: .env.schema ```env-spec # @initInfisical(projectId=my-project, environment=production, clientId=$ID, clientSecret=$SECRET) # --- DB_PASSWORD=infisical("DB_PASSWORD", "/database") API_KEY=infisical("API_KEY", "/api") ``` ### Bulk loading secrets [Section titled “Bulk loading secrets”](#bulk-loading-secrets) Use `infisicalBulk()` with `@setValuesBulk` to load all secrets from a project environment at once, instead of wiring up each secret individually: .env.schema ```env-spec # @plugin(@varlock/infisical-plugin) # @initInfisical(projectId=my-project, environment=dev, clientId=$INFISICAL_CLIENT_ID, clientSecret=$INFISICAL_CLIENT_SECRET) # @setValuesBulk(infisicalBulk()) # --- # @type=infisicalClientId INFISICAL_CLIENT_ID= # @type=infisicalClientSecret @sensitive @internal INFISICAL_CLIENT_SECRET= API_KEY= DB_PASSWORD= ``` You can filter by path and/or tag: .env.schema ```env-spec # Load secrets from a specific path # @setValuesBulk(infisicalBulk(path="/database")) # Load secrets with a specific tag # @setValuesBulk(infisicalBulk(tag="backend")) # Combine path and tag # @setValuesBulk(infisicalBulk(path="/production", tag="app")) # With a named instance # @setValuesBulk(infisicalBulk(prod, path="/database")) ``` *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initInfisical()` [Section titled “@initInfisical()”](#initinfisical) Initialize an Infisical plugin instance for accessing secrets. **Key/value args:** * `projectId` (required): Infisical project ID * `environment` (required): Environment name (e.g., `dev`, `staging`, `production`) * `clientId` (required): Universal Auth Client ID. Should be a reference to a config item of type `infisicalClientId`. * `clientSecret` (required): Universal Auth Client Secret. Should be a reference to a config item of type `infisicalClientSecret`. * `siteUrl` (optional): Custom Infisical instance URL (defaults to `https://app.infisical.com`) * `identityId` (optional): Machine identity ID for OIDC authentication (alternative to `clientId`/`clientSecret`) * `oidcToken` (optional): Explicit OIDC JWT token (auto-detected from platform if omitted) * `secretPath` (optional): Default secret path for all secrets (defaults to `/`) * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). * `allowMissing` (optional): if `true`, missing secrets return `undefined` instead of throwing. Can be a dynamic value (e.g. `allowMissing=forEnv(development)`). Other errors (auth failures, permission errors, etc.) still throw. * `id` (optional): Instance identifier for multiple instances ```env-spec # @initInfisical( # projectId=your-project-id, # environment=dev, # clientId=$INFISICAL_CLIENT_ID, # clientSecret=$INFISICAL_CLIENT_SECRET, # cacheTtl="1h" # ) # --- # @type=infisicalClientId INFISICAL_CLIENT_ID= # @type=infisicalClientSecret @sensitive @internal INFISICAL_CLIENT_SECRET= ``` ### Data types [Section titled “Data types”](#data-types) #### `infisicalClientId` [Section titled “infisicalClientId”](#infisicalclientid) Represents an Infisical Universal Auth Client ID. This is not marked as sensitive. ```env-spec # @type=infisicalClientId INFISICAL_CLIENT_ID= ``` #### `infisicalClientSecret` [Section titled “infisicalClientSecret”](#infisicalclientsecret) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents an Infisical Universal Auth Client Secret. This type is marked as `@sensitive`. ```env-spec # @type=infisicalClientSecret INFISICAL_CLIENT_SECRET= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `infisical()` [Section titled “infisical()”](#infisical) Fetch a secret from Infisical. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `secretName` (optional): secret name in Infisical. If omitted, uses the variable name. * `secretPath` (optional): path to the secret (overrides default path) **Key/value args:** * `allowMissing` (optional): if `true`, returns `undefined` instead of throwing when the secret is not found. Can be a dynamic value (e.g. `allowMissing=forEnv(development)`). Useful with `fallback()` to provide a default value. Other errors still throw. ```env-spec # Auto-infer secret name from variable DATABASE_URL=infisical() # Explicit secret name STRIPE_KEY=infisical("STRIPE_SECRET_KEY") # With custom path DB_PASSWORD=infisical("DB_PASSWORD", "/database") # With instance ID DEV_SECRET=infisical(dev, "DATABASE_URL") # Full form PROD_SECRET=infisical(prod, "DATABASE_URL", "/production") # Optional secret with fallback RESEND_API_KEY=fallback(infisical(allowMissing=true), undefined) ``` #### `infisicalBulk()` [Section titled “infisicalBulk()”](#infisicalbulk) Bulk-load all secrets from an Infisical project environment. Intended for use with `@setValuesBulk`. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized **Key/value args:** * `path` (optional): secret path to fetch from (overrides default path from `@initInfisical`) * `tag` (optional): tag slug to filter secrets by ```env-spec # Load all secrets from default instance # @setValuesBulk(infisicalBulk()) # Load from a specific path # @setValuesBulk(infisicalBulk(path="/database")) # Filter by tag # @setValuesBulk(infisicalBulk(tag="backend")) # With instance ID and path # @setValuesBulk(infisicalBulk(prod, path="/database")) ``` *** ## Example Configurations [Section titled “Example Configurations”](#example-configurations) ### Development setup with auto-named secrets [Section titled “Development setup with auto-named secrets”](#development-setup-with-auto-named-secrets) .env.schema ```env-spec # @plugin(@varlock/infisical-plugin) # @initInfisical(projectId=dev-app, environment=dev, clientId=$INFISICAL_CLIENT_ID, clientSecret=$INFISICAL_CLIENT_SECRET) # --- # @type=infisicalClientId INFISICAL_CLIENT_ID= # @type=infisicalClientSecret @sensitive @internal INFISICAL_CLIENT_SECRET= # Secret names automatically match config keys DATABASE_URL=infisical() REDIS_URL=infisical() STRIPE_KEY=infisical() ``` ### Production with path organization [Section titled “Production with path organization”](#production-with-path-organization) .env.schema ```env-spec # @plugin(@varlock/infisical-plugin) # @initInfisical( # projectId=prod-app, # environment=production, # clientId=$INFISICAL_CLIENT_ID, # clientSecret=$INFISICAL_CLIENT_SECRET, # secretPath=/production # ) # --- # Database secrets at /production/database DB_HOST=infisical("DB_HOST", "/database") DB_PASSWORD=infisical("DB_PASSWORD", "/database") # API keys at /production/api STRIPE_KEY=infisical("STRIPE_KEY", "/api") SENDGRID_KEY=infisical("SENDGRID_KEY", "/api") ``` ### Multi-region setup [Section titled “Multi-region setup”](#multi-region-setup) .env.schema ```env-spec # @plugin(@varlock/infisical-plugin) # @initInfisical(id=us, projectId=app-us, environment=production, clientId=$US_CLIENT_ID, clientSecret=$US_CLIENT_SECRET) # @initInfisical(id=eu, projectId=app-eu, environment=production, clientId=$EU_CLIENT_ID, clientSecret=$EU_CLIENT_SECRET) # --- US_DATABASE=infisical(us, "DATABASE_URL") EU_DATABASE=infisical(eu, "DATABASE_URL") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Secret not found [Section titled “Secret not found”](#secret-not-found) * Verify the secret exists in your Infisical project and environment * Check the secret name matches exactly (including case) * Verify the secret path is correct if using paths * Ensure your machine identity has access to the secret * If the secret is genuinely optional, set `allowMissing=true` on `infisical()` or `@initInfisical()` and use `@required=false` or `fallback()` ### Access denied [Section titled “Access denied”](#access-denied) * Check that your machine identity has been granted access to the project and environment * Verify the machine identity permissions in Infisical console * Ensure the project ID and environment match your configuration ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * Verify the client ID and client secret are correct * Check if the machine identity has been revoked or disabled * For self-hosted: verify `siteUrl` is correct ### Wrong environment [Section titled “Wrong environment”](#wrong-environment) * Double-check the `environment` parameter matches the environment where your secret is stored * Remember that secrets in Infisical are environment-specific ## Resources [Section titled “Resources”](#resources) * [Infisical Documentation](https://infisical.com/docs) * [Machine Identities](https://infisical.com/docs/documentation/platform/identities/machine-identities) * [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) * [Infisical Node SDK](https://infisical.com/docs/sdks/languages/node) # KeePass Plugin > Using KeePass / KeePassXC databases with Varlock [![](https://img.shields.io/npm/v/@varlock/keepass-plugin?label=%40varlock%2Fkeepass-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/keepass-plugin) Our [KeePass](https://keepass.info/) plugin enables loading secrets from KDBX 4.0 database files using declarative instructions within your `.env` files. It supports two modes of operation: **file mode** (default) reads `.kdbx` files directly using a pure WASM implementation, and **CLI mode** uses `keepassxc-cli` for development workflows with system-level key management (YubiKey, Windows Hello, etc.). ## Features [Section titled “Features”](#features) * **KDBX 4.0 support**: reads KeePass database files directly via [kdbxweb](https://github.com/keeweb/kdbxweb) with pure WASM argon2 * **KeePassXC CLI integration**: use `keepassxc-cli` for development workflows * **Key file support**: authenticate with password + optional key file * **`#attribute` syntax**: read any entry field (Password, UserName, URL, Notes, or custom fields) * **Auto-infer entry paths** from variable names for convenience * **Custom attributes object**: load all custom fields from a single entry for `@setValuesBulk` * **Bulk loading** with `kpBulk()` via `@setValuesBulk` to load all entries in a group * **Multiple instances** for accessing different databases ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/keepass-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/keepass-plugin) # # 2. Initialize with your database path and password # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD) ``` ### With key file [Section titled “With key file”](#with-key-file) .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD, keyFile="./secrets.keyx") ``` ### CLI mode [Section titled “CLI mode”](#cli-mode) When `useCli=true`, the plugin uses `keepassxc-cli` to read entries instead of reading the file directly. This is useful during development when you want to use KeePassXC’s system integration. The `useCli` option can be dynamic. For example, `useCli=forEnv(dev)` uses the CLI only in development: .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD, useCli=forEnv(dev)) ``` You must have KeePassXC installed, which includes `keepassxc-cli`: ```bash # macOS brew install --cask keepassxc # Ubuntu/Debian sudo apt install keepassxc # Fedora/RHEL sudo dnf install keepassxc # Arch pacman -S keepassxc ``` See [KeePassXC downloads](https://keepassxc.org/download/) for more options. ### Multiple databases [Section titled “Multiple databases”](#multiple-databases) Access secrets from multiple databases using the `id` parameter: .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(id=prod, dbPath="./prod.kdbx", password=$KP_PROD_PASSWORD) # @initKeePass(id=dev, dbPath="./dev.kdbx", password=$KP_DEV_PASSWORD) # --- PROD_SECRET=kp(prod, "Database/production") DEV_SECRET=kp(dev, "Database/development") ``` Note The plugin does **not** fail at load time if the database credentials are invalid. It only fails when you actually try to read a secret, making it safe to include in shared configs. ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `kp()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) Fetch secrets using entry paths with `#attribute` syntax: .env.schema ```env-spec # Fetch password (default attribute) from an entry DB_PASSWORD=kp("Database/production") # Fetch a different attribute using #attribute syntax DB_USER=kp("Database/production#UserName") DB_URL=kp("Database/production#URL") # Read a custom string field API_KEY=kp("Services/stripe#SecretKey") ``` ### Inferring entry name from key [Section titled “Inferring entry name from key”](#inferring-entry-name-from-key) When the env var key matches a KeePass entry title, you can omit the entry path: .env.schema ```env-spec # Looks up entry titled "DB_PASSWORD", reads the Password field DB_PASSWORD=kp() # Looks up entry titled "DB_USER", reads the UserName field DB_USER=kp("#UserName") ``` ### Entry paths [Section titled “Entry paths”](#entry-paths) Entry paths use forward slashes to separate groups from the entry title: ```plaintext Group/SubGroup/EntryTitle ``` For example, if your KeePass database has: * Root * Database * production (entry with Password, UserName fields) * Services * stripe (entry with a custom “SecretKey” field) You would reference them as `"Database/production"` and `"Services/stripe"`. ### Custom attributes object [Section titled “Custom attributes object”](#custom-attributes-object) Use `customAttributesObj=true` to load all custom (non-standard) fields from a single entry as a JSON object. This is useful with `@setValuesBulk` to expand custom fields into env vars: .env.schema ```env-spec # Given an entry "Database/production" with custom fields: HOST, PORT, DB_NAME # @setValuesBulk(kp("Database/production", customAttributesObj=true), createMissing=true) # --- HOST= PORT= DB_NAME= ``` Standard fields (Title, Password, UserName, URL, Notes) are excluded; only custom fields are included. ### Bulk loading secrets [Section titled “Bulk loading secrets”](#bulk-loading-secrets) Use `kpBulk()` with `@setValuesBulk` to fetch the Password field from all entries under a group: .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD) # @setValuesBulk(kpBulk(Production), createMissing=true) # --- # These will be populated from entries under the "Production" group DB_PASSWORD= API_KEY= ``` You can customize the scope: .env.schema ```env-spec # Load all entries from the database root # @setValuesBulk(kpBulk()) # Load from a specific group # @setValuesBulk(kpBulk("Services/APIs")) # With a named instance # @setValuesBulk(kpBulk(prod, Production)) ``` Entry paths in the JSON output are sanitized to valid env var names (uppercased, non-alphanumeric characters replaced with underscores). *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initKeePass()` [Section titled “@initKeePass()”](#initkeepass) Initialize a KeePass plugin instance for accessing secrets from a KDBX database. **Key/value args:** * `dbPath` **(required)**: Path to the `.kdbx` database file * `password` **(required)**: Master password (typically from an env var like `$KP_PASSWORD`) * `keyFile` (optional): Path to a key file for additional authentication * `useCli` (optional): Use `keepassxc-cli` instead of reading the file directly (default: `false`). Can be dynamic, e.g. `useCli=forEnv(dev)` * `id` (optional): Instance identifier for multiple databases ```env-spec # Basic setup # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD) # With key file and CLI mode # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD, keyFile="./key.keyx", useCli=true) # Multiple instances # @initKeePass(id=prod, dbPath="./prod.kdbx", password=$KP_PROD_PW) ``` ### Data types [Section titled “Data types”](#data-types) #### `kdbxPassword` [Section titled “kdbxPassword”](#kdbxpassword) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. A sensitive string type for KeePass database master passwords. Validates that the value is a non-empty string and is automatically marked as sensitive. ```env-spec # @type=kdbxPassword @sensitive @internal KP_PASSWORD= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `kp()` [Section titled “kp()”](#kp) Fetch a single entry field from a KeePass database. Returns the Password field by default. **Array args:** * `instanceId` (optional): instance identifier when multiple plugin instances are initialized * `entryPath` (optional): path to the entry (uses `#attribute` for non-Password fields). If omitted, uses the variable name. **Key/value args:** * `attribute` (optional): alternative to `#attribute` syntax for specifying the field to read * `customAttributesObj` (optional): if `true`, returns all custom fields as a JSON object (for use with `@setValuesBulk`) ```env-spec # Default password field DB_PASSWORD=kp("Database/production") # Specific attribute via #attribute DB_USER=kp("Database/production#UserName") # Infer entry name from variable key DB_PASSWORD=kp() # Infer entry name + specify attribute DB_USER=kp("#UserName") # Custom fields as JSON object # @setValuesBulk(kp("Database/production", customAttributesObj=true)) # With instance ID SECRET=kp(prod, "Database/production") ``` #### `kpBulk()` [Section titled “kpBulk()”](#kpbulk) Fetch the Password field from all entries under a group as a JSON map. Intended for use with `@setValuesBulk`. Entry paths are sanitized to valid env var names (uppercased, non-alphanumeric replaced with underscores). **Array args:** * `instanceId` (optional): instance identifier when multiple plugin instances are initialized * `groupPath` (optional): group path to load entries from (loads all entries if omitted) ```env-spec # Load all entries from the database root # @setValuesBulk(kpBulk()) # Load entries under a specific group # @setValuesBulk(kpBulk(Production)) # With instance ID # @setValuesBulk(kpBulk(prod, Production)) ``` *** ## Example configurations [Section titled “Example configurations”](#example-configurations) ### Simple setup with password from env [Section titled “Simple setup with password from env”](#simple-setup-with-password-from-env) .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD) # --- # @type=kdbxPassword @sensitive @internal KP_PASSWORD= DB_PASSWORD=kp("Database/production") API_KEY=kp("Services/stripe#SecretKey") SMTP_USER=kp("Email/smtp#UserName") ``` ### Bulk loading a group [Section titled “Bulk loading a group”](#bulk-loading-a-group) .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD) # @setValuesBulk(kpBulk(Production), createMissing=true) # --- # @type=kdbxPassword @sensitive @internal KP_PASSWORD= # These are auto-populated from entries in the "Production" group DB_PASSWORD= API_KEY= REDIS_URL= ``` ### Custom fields from a single entry [Section titled “Custom fields from a single entry”](#custom-fields-from-a-single-entry) .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD) # @setValuesBulk(kp("Database/production", customAttributesObj=true), createMissing=true) # --- # @type=kdbxPassword @sensitive @internal KP_PASSWORD= # Custom fields from the "Database/production" entry HOST= PORT= DB_NAME= ``` ### Dev CLI / prod file mode [Section titled “Dev CLI / prod file mode”](#dev-cli--prod-file-mode) .env.schema ```env-spec # @plugin(@varlock/keepass-plugin) # @initKeePass(dbPath="./secrets.kdbx", password=$KP_PASSWORD, useCli=forEnv(dev)) # --- # @type=kdbxPassword @sensitive @internal KP_PASSWORD= # In dev: reads via keepassxc-cli (YubiKey, etc.) # In prod: reads the .kdbx file directly DB_PASSWORD=kp("Database/production") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Invalid credentials [Section titled “Invalid credentials”](#invalid-credentials) * Check that the database password is correct * If using a key file, verify the path is correct and the file matches the database ### Entry not found [Section titled “Entry not found”](#entry-not-found) * Entry paths are case-sensitive * Use forward slashes to separate groups: `"Group/SubGroup/Entry"` * In CLI mode, list entries with: `keepassxc-cli ls ` ### `keepassxc-cli` not found (CLI mode only) [Section titled “keepassxc-cli not found (CLI mode only)”](#keepassxc-cli-not-found-cli-mode-only) * Install KeePassXC which includes the CLI (see [CLI mode](#cli-mode)) * Ensure `keepassxc-cli` is in your `PATH` ### Database file not found [Section titled “Database file not found”](#database-file-not-found) * Check the `dbPath` value; it’s resolved relative to the working directory * Use an absolute path if needed ## Resources [Section titled “Resources”](#resources) * [KeePassXC](https://keepassxc.org/): cross-platform KeePass-compatible password manager * [KeePass](https://keepass.info/): original KeePass Password Safe * [KDBX format](https://keepass.info/help/kb/kdbx.html): KeePass database format specification * [kdbxweb](https://github.com/keeweb/kdbxweb): JavaScript KDBX reader library # Keeper Plugin > Using Keeper Security Secrets Manager with Varlock [![](https://img.shields.io/npm/v/@varlock/keeper-plugin?label=%40varlock%2Fkeeper-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/keeper-plugin) Our [Keeper Security](https://keepersecurity.com/) plugin loads secrets from Keeper vaults via the [Keeper Secrets Manager](https://docs.keeper.io/secrets-manager/) SDK. The plugin uses base64-encoded configuration tokens for programmatic access to your Keeper secrets, making it suitable for both CI/CD and production environments. ## Features [Section titled “Features”](#features) * **SDK-based authentication**: access via base64-encoded Secrets Manager config tokens * **Secret access** by record UID, title, `#field` selector, or [Keeper notation](https://docs.keeper.io/secrets-manager/secrets-manager/developer-sdk-library/javascript-sdk#notation) * **Standard and custom fields**: access any field type including password, login, URL, notes, and custom fields * **Multiple instances**: connect to different Keeper applications or vaults at once * **Opt-in secret caching** via `cacheTtl`: see the [Caching guide](/guides/caching/) * **Error handling** with helpful tips ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/keeper-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/keeper-plugin) # # 2. Initialize the plugin - see below for more details on options # @initKeeper(token=$KSM_CONFIG) # --- # 3. Add a config item for the Secrets Manager config token # @type=keeperSmToken @sensitive @internal KSM_CONFIG= ``` ### Secrets Manager application setup [Section titled “Secrets Manager application setup”](#secrets-manager-application-setup) 1. **Create a Secrets Manager Application** in the Keeper Admin Console Navigate to **Secrets Manager** → **Applications** → Click **Create Application**. Give it a descriptive name (e.g., “varlock-prod”). 2. **Share a folder** with the application In your Keeper vault, right-click the folder containing your secrets, select **Share Folder**, and share it with your Secrets Manager application. 3. **Generate a one-time access token** In the application settings, click **Add Device** and copy the one-time access token. 4. **Initialize and export the config** Use the [KSM CLI](https://docs.keeper.io/secrets-manager/secrets-manager/secrets-manager-command-line-interface) to create a config: ```bash pip install keeper-secrets-manager-cli ksm profile init ksm profile export --format json | base64 ``` Save the config securely Store the base64-encoded config token securely. The one-time access token can only be used once to initialize a device. 5. **Wire up the token in your config** .env.schema ```env-spec # @plugin(@varlock/keeper-plugin) # @initKeeper(token=$KSM_CONFIG) # --- # @type=keeperSmToken @sensitive @internal KSM_CONFIG= ``` 6. **Set your config token in environments** Use your CI/CD system or platform’s env var management to securely inject the `KSM_CONFIG` value. ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to different Keeper applications or vaults, register multiple named instances: .env.schema ```env-spec # @plugin(@varlock/keeper-plugin) # @initKeeper(token=$KSM_PROD, id=prod) # @initKeeper(token=$KSM_DEV, id=dev) # --- # @type=keeperSmToken @sensitive @internal KSM_PROD= # @type=keeperSmToken @sensitive @internal KSM_DEV= PROD_SECRET=keeper(prod, "XXXXXXXXXXXXXXXXXXXX") DEV_SECRET=keeper(dev, "XXXXXXXXXXXXXXXXXXXX") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `keeper()` resolver function. ### By record UID [Section titled “By record UID”](#by-record-uid) Fetch secrets by record UID. By default, the `password` field is returned: .env.schema ```env-spec # Fetches the "password" field from the record DB_PASSWORD=keeper("XXXXXXXXXXXXXXXXXXXX") ``` ### With a field selector [Section titled “With a field selector”](#with-a-field-selector) Use the `#` syntax to access a specific field: .env.schema ```env-spec # Standard fields DB_USER=keeper("XXXXXXXXXXXXXXXXXXXX#login") SITE_URL=keeper("XXXXXXXXXXXXXXXXXXXX#url") # Custom fields by label API_KEY=keeper("XXXXXXXXXXXXXXXXXXXX#API_KEY") ``` Or use the named `field` parameter: .env.schema ```env-spec DB_HOST=keeper("XXXXXXXXXXXXXXXXXXXX", field="host") ``` ### Using Keeper notation [Section titled “Using Keeper notation”](#using-keeper-notation) The plugin supports [Keeper’s notation syntax](https://docs.keeper.io/secrets-manager/secrets-manager/developer-sdk-library/javascript-sdk#notation) for advanced access patterns: .env.schema ```env-spec # Standard field by type DB_PASS=keeper("XXXX/field/password") # Login field DB_USER=keeper("XXXX/field/login") # Custom field by label MY_SECRET=keeper("XXXX/custom_field/MySecretLabel") # By record title instead of UID API_KEY=keeper("My API Keys/field/password") ``` ### With named instances [Section titled “With named instances”](#with-named-instances) When using multiple instances, specify which one to use as the first argument: .env.schema ```env-spec PROD_SECRET=keeper(prod, "XXXX/field/password") DEV_SECRET=keeper(dev, "YYYY#password") ``` Record UIDs vs titles You can reference records by either their UID or their title. UIDs are more reliable since titles can be changed, but titles are more readable. UIDs are case-sensitive. *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initKeeper()` [Section titled “@initKeeper()”](#initkeeper) Initialize a Keeper Secrets Manager plugin instance for accessing secrets. **Key/value args:** * `token` (required): Base64-encoded Secrets Manager config token. Should be a reference to a config item of type `keeperSmToken`. * `id` (optional): Instance identifier for multiple instances * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). ```env-spec # @initKeeper(token=$KSM_CONFIG) # --- # @type=keeperSmToken @sensitive @internal KSM_CONFIG= ``` ### Data types [Section titled “Data types”](#data-types) #### `keeperSmToken` [Section titled “keeperSmToken”](#keepersmtoken) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents a base64-encoded configuration token for the Keeper Secrets Manager SDK. Validation ensures the value is a valid base64-encoded JSON string. Note that the type itself is marked as `@sensitive`, so adding an explicit `@sensitive` decorator is optional. ```env-spec # @type=keeperSmToken KSM_CONFIG= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `keeper()` [Section titled “keeper()”](#keeper) Fetch a secret field from Keeper Secrets Manager. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `reference` (required): record UID, title, UID with `#field` selector, or Keeper notation string **Key/value args:** * `field` (optional): explicit field type or label to extract from the record ```env-spec # By record UID (defaults to password field) DB_PASSWORD=keeper("XXXXXXXXXXXXXXXXXXXX") # With field selector DB_USER=keeper("XXXXXXXXXXXXXXXXXXXX#login") # With named field parameter DB_HOST=keeper("XXXXXXXXXXXXXXXXXXXX", field="host") # Using Keeper notation API_KEY=keeper("XXXX/field/password") CUSTOM=keeper("XXXX/custom_field/API_KEY") # With instance ID PROD_SECRET=keeper(prod, "XXXXXXXXXXXXXXXXXXXX") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Failed to parse config token [Section titled “Failed to parse config token”](#failed-to-parse-config-token) * The `KSM_CONFIG` value must be a valid base64-encoded JSON string * Regenerate it using the KSM CLI: ```bash ksm profile export --format json | base64 ``` ### Access denied [Section titled “Access denied”](#access-denied) * Verify the Secrets Manager application has not been revoked * Check that the shared folder permissions are still active * The one-time token may have expired before being used; generate a new one ### Record not found [Section titled “Record not found”](#record-not-found) * Verify the record UID or title is correct * Ensure the record is in a folder shared with the Secrets Manager application * Record UIDs are case-sensitive ### Field not found in record [Section titled “Field not found in record”](#field-not-found-in-record) * Check available field types: `login`, `password`, `url`, `oneTimeCode`, `note` * Custom fields use the label: `keeper("uid/custom_field/My Label")` * Use the `#` syntax for quick field access: `keeper("uid#login")` ### Config token issues [Section titled “Config token issues”](#config-token-issues) * One-time access tokens can only be used once to initialize a device * If you need a new config, create a new device in the application settings * Config tokens contain encrypted keys; do not modify them manually ## Resources [Section titled “Resources”](#resources) * [Keeper Security](https://keepersecurity.com/) * [Keeper Secrets Manager](https://docs.keeper.io/secrets-manager/) * [JavaScript SDK Documentation](https://docs.keeper.io/secrets-manager/secrets-manager/developer-sdk-library/javascript-sdk) * [KSM CLI Documentation](https://docs.keeper.io/secrets-manager/secrets-manager/secrets-manager-command-line-interface) # Kubernetes Plugin > Using Kubernetes Secrets and ConfigMaps with Varlock [![](https://img.shields.io/npm/v/@varlock/kubernetes-plugin?label=%40varlock%2Fkubernetes-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/kubernetes-plugin) Our Kubernetes plugin enables loading values from Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) and [ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/) using declarative instructions within your `.env` files. ## Scope [Section titled “Scope”](#scope) This plugin is **read-only**. It performs `get` requests on Secrets and ConfigMaps in a configured namespace and surfaces the values to your `.env` schema, nothing more. It does **not** create, update, or delete cluster resources, generate or template manifests, watch for changes, or manage deployments. **Typical use cases:** * **Local development**: pull dev/staging Secrets and ConfigMaps from a cluster into your local app without copying values by hand * **In-cluster runtime**: read additional Secrets/ConfigMaps at runtime that aren’t already mounted into the pod via `envFrom` / `valueFrom` * **CI/CD**: read Secrets/ConfigMaps from a cluster using an explicit service account token It supports local kubeconfig, in-cluster service account credentials, and explicit API server + token authentication. Want deeper Kubernetes integration? We’re considering broader Kubernetes support (manifest generation, schema-to-deployment validation, change watching, etc.). If you have a use case this plugin doesn’t cover, or feedback from running it in production, come chat on [Discord](https://chat.dmno.dev). We’d love to hear from you. ## Features [Section titled “Features”](#features) * **Zero-config local development**: automatically uses your default kubeconfig (`~/.kube/config`) * **In-cluster authentication**: auto-detects the mounted service account when running inside a pod * **Explicit auth**: provide a cluster API URL and bearer token directly * **Fetch Secret keys** with `k8sSecret()` (values are automatically base64-decoded) * **Fetch ConfigMap keys** with `k8sConfigMap()` (including `binaryData`) * **Bulk-load** whole Secrets or ConfigMaps with `k8sSecretBulk()` / `k8sConfigMapBulk()` * **Auto-infer keys** from environment variable names * **Multiple instances** for different namespaces or clusters * **Read-only**: the plugin never mutates cluster state ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/kubernetes-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/kubernetes-plugin) # # 2. Initialize the plugin - see below for more details on options # @initKubernetes(namespace=default) ``` ### Authentication options [Section titled “Authentication options”](#authentication-options) The plugin tries authentication methods in this priority order: 1. **Explicit cluster server + token** - If `clusterServer` is provided in `@initKubernetes()` 2. **Explicit kubeconfig** - If `kubeconfig` is provided (file path or raw YAML/JSON string) 3. **In-cluster service account** - Auto-detected via `KUBERNETES_SERVICE_HOST`/`KUBERNETES_SERVICE_PORT` env vars (set automatically inside pods) 4. **Default kubeconfig** - Loads from `$KUBECONFIG` or `~/.kube/config` ### Automatic authentication (Recommended for local dev) [Section titled “Automatic authentication (Recommended for local dev)”](#automatic-authentication-recommended-for-local-dev) For local development, just initialize the plugin and the plugin will pick up your default kubeconfig automatically: .env.schema ```env-spec # @plugin(@varlock/kubernetes-plugin) # @initKubernetes(namespace=default) ``` **How this works:** * **Local development:** Uses your active `kubectl` context from `~/.kube/config` (or `$KUBECONFIG`) * **Inside a pod:** Uses the pod’s mounted service account credentials at `/var/run/secrets/kubernetes.io/serviceaccount/` If your kubeconfig has multiple contexts, you can select one explicitly: .env.schema ```env-spec # @initKubernetes(namespace=default, context=my-dev-cluster) ``` ### Explicit cluster server and token [Section titled “Explicit cluster server and token”](#explicit-cluster-server-and-token) If you don’t want to rely on a kubeconfig file (for example, in CI/CD or other deployed environments), provide the API server URL and a bearer token directly: 1. **Create a service account and RBAC** in your cluster (see Kubernetes Setup section below) 2. **Wire up the credentials in your config**. Add a config item for the token and reference it when initializing the plugin. .env.schema ```env-spec # @plugin(@varlock/kubernetes-plugin) # @initKubernetes( # namespace=default, # clusterServer="https://kubernetes.example.com:6443", # token=$KUBERNETES_TOKEN # ) # --- # @type=kubernetesBearerToken @sensitive @internal KUBERNETES_TOKEN= ``` 3. **Set your credentials in deployed environments**. Use your platform’s env var management UI to securely inject the bearer token. For clusters that present self-signed or custom-CA certificates, you can disable TLS verification by adding `skipTlsVerify=true` to `@initKubernetes()`. This should generally be avoided outside of development. ### Raw kubeconfig [Section titled “Raw kubeconfig”](#raw-kubeconfig) You can also pass an entire kubeconfig as a string (YAML or JSON), useful when injecting credentials from a secret manager or platform env var: .env.schema ```env-spec # @initKubernetes(kubeconfig=$KUBECONFIG_DATA) # --- # @sensitive KUBECONFIG_DATA= ``` The plugin auto-detects whether `kubeconfig` is a file path or raw content by looking for YAML/JSON markers. ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to read from multiple namespaces or clusters, register multiple named instances: .env.schema ```env-spec # @initKubernetes(id=dev, namespace=dev) # @initKubernetes(id=prod, namespace=prod, context=prod-cluster) # --- DEV_DATABASE_URL=k8sSecret(dev, app-secrets, DATABASE_URL) PROD_DATABASE_URL=k8sSecret(prod, app-secrets, DATABASE_URL) ``` ## Loading values [Section titled “Loading values”](#loading-values) Once the plugin is installed and initialized, you can start adding config items that load values using the `k8sSecret()` and `k8sConfigMap()` resolver functions. ### How Secrets and ConfigMaps are structured [Section titled “How Secrets and ConfigMaps are structured”](#how-secrets-and-configmaps-are-structured) A Kubernetes Secret or ConfigMap is a **named resource that holds a map of key/value pairs**: Example Secret ```yaml apiVersion: v1 kind: Secret metadata: name: app-secrets # ← the resource name data: DATABASE_URL: cG9zdGdyZXM6... # ← keys inside the resource API_KEY: c2VjcmV0LWtleQ== ``` So fetching a value is always a two-level lookup: which Secret/ConfigMap (`name`), and which key inside it (`key`). The resolvers reflect this directly: * `k8sSecret(name, key)`: read `name.data.key` * `k8sConfigMap(name, key)`: read `name.data.key` (or `name.binaryData.key`) When `key` is omitted, the plugin uses the config item’s name as the key. This works well when your Secret keys already match your env var names. ### Secret keys [Section titled “Secret keys”](#secret-keys) The `k8sSecret()` function fetches a key from a Kubernetes Secret. Values stored in Secret `data` are base64-encoded; the plugin decodes them automatically before returning. .env.schema ```env-spec # Auto-infer key from item name (fetches "DATABASE_URL" from "app-secrets") DATABASE_URL=k8sSecret(app-secrets) # Explicit key name DB_URL=k8sSecret(app-secrets, DATABASE_URL) # Named args also work DB_URL=k8sSecret(name=app-secrets, key=DATABASE_URL) # With named instance PROD_DB_URL=k8sSecret(prod, app-secrets, DATABASE_URL) ``` ### ConfigMap keys [Section titled “ConfigMap keys”](#configmap-keys) The `k8sConfigMap()` function fetches a key from a Kubernetes ConfigMap. Both `data` (string) and `binaryData` (base64-encoded) fields are supported. .env.schema ```env-spec # Auto-infer key from item name PUBLIC_API_HOST=k8sConfigMap(app-config) # Explicit key name API_HOST=k8sConfigMap(app-config, PUBLIC_API_HOST) ``` ### Default Secret/ConfigMap (Recommended for the common case) [Section titled “Default Secret/ConfigMap (Recommended for the common case)”](#default-secretconfigmap-recommended-for-the-common-case) The idiomatic Kubernetes deployment pattern is one Secret + one ConfigMap per app, mounted into the pod via `envFrom`. If your app follows this pattern, set `defaultSecret` and `defaultConfigMap` once on the init decorator and skip the name argument on every call: .env.schema ```env-spec # @plugin(@varlock/kubernetes-plugin) # @initKubernetes( # namespace=default, # defaultSecret=app-secrets, # defaultConfigMap=app-config, # ) # --- # Both default to app-secrets / app-config and infer the key from the item name DATABASE_URL=k8sSecret() API_KEY=k8sSecret() JWT_SECRET=k8sSecret() PUBLIC_API_HOST=k8sConfigMap() # Override just the key while still using the default Secret STRIPE_KEY=k8sSecret(key=stripe_api_key) # Override the resource name to read from a different Secret SHARED_TOKEN=k8sSecret(shared-secrets, AUTH_TOKEN) ``` You can mix positional and named arguments, but you can’t provide the same field twice. For example, `k8sSecret(app-secrets, name=other-secrets)` is a schema error. ### Bulk loading [Section titled “Bulk loading”](#bulk-loading) Use bulk loading when a single Secret or ConfigMap contains several environment variables you want to map into your config. The bulk resolvers return all keys as a JSON object, which pairs naturally with `@setValuesBulk`: .env.schema ```env-spec # @plugin(@varlock/kubernetes-plugin) # @initKubernetes(namespace=default) # @setValuesBulk(k8sSecretBulk(app-secrets), format=json) # @setValuesBulk(k8sConfigMapBulk(app-config), format=json) # --- DATABASE_URL= API_KEY= PUBLIC_API_HOST= ``` Only items declared in your schema will be populated; any extra keys in the Secret or ConfigMap are ignored. Bulk resolvers also pick up `defaultSecret`/`defaultConfigMap`, so the names can be omitted entirely: .env.schema ```env-spec # @initKubernetes(defaultSecret=app-secrets, defaultConfigMap=app-config) # @setValuesBulk(k8sSecretBulk(), format=json) # @setValuesBulk(k8sConfigMapBulk(), format=json) ``` ### Optional values [Section titled “Optional values”](#optional-values) By default, fetching a key from a missing Secret/ConfigMap throws an error. If you want missing resources or keys to resolve to `undefined` instead, set `allowMissing=true`: .env.schema ```env-spec # @initKubernetes(namespace=default, allowMissing=true) # --- # @required=false OPTIONAL_FLAG=k8sConfigMap(feature-flags, NEW_UI) ``` When `allowMissing=true`, also mark the corresponding items with `@required=false` (or wrap the resolver with `fallback()`) so validation does not fail. *** ## Kubernetes Setup [Section titled “Kubernetes Setup”](#kubernetes-setup) ### Required RBAC permissions [Section titled “Required RBAC permissions”](#required-rbac-permissions) The identity used by the plugin (your kubeconfig user, an in-cluster service account, or an explicit token) needs read access to Secrets and/or ConfigMaps in the target namespace. The minimum permissions are `get` on `secrets` and `configmaps`: varlock-rbac.yaml ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: varlock-reader namespace: default rules: - apiGroups: [""] resources: ["secrets", "configmaps"] verbs: ["get"] ``` Apply it with: ```bash kubectl apply -f varlock-rbac.yaml ``` Least privilege principle Scope down `resources` to only what you need. If you’re only reading ConfigMaps, omit `"secrets"` entirely. For multi-namespace access, use a `ClusterRole` + `ClusterRoleBinding` instead, but prefer namespaced `Role`s whenever possible. ### Service account for in-cluster use [Section titled “Service account for in-cluster use”](#service-account-for-in-cluster-use) When running inside a pod, the plugin uses the pod’s mounted service account. Create a dedicated service account and bind it to the role above: 1. **Create a service account** ```bash kubectl create serviceaccount varlock-reader -n default ``` 2. **Bind the role to the service account** varlock-rolebinding.yaml ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: varlock-reader-binding namespace: default subjects: - kind: ServiceAccount name: varlock-reader namespace: default roleRef: kind: Role name: varlock-reader apiGroup: rbac.authorization.k8s.io ``` ```bash kubectl apply -f varlock-rolebinding.yaml ``` 3. **Use the service account in your pod spec** deployment.yaml ```yaml spec: serviceAccountName: varlock-reader containers: - name: app image: my-app:latest ``` ### Generate a bearer token for explicit auth [Section titled “Generate a bearer token for explicit auth”](#generate-a-bearer-token-for-explicit-auth) For CI/CD or other external use cases that need an explicit token, create a long-lived service account token: ```bash # Create a token secret bound to the service account kubectl apply -f - < -n ` or `kubectl get configmap -n ` * Double-check the namespace; the plugin reads only from the configured namespace * Resource names are case-sensitive and namespace-scoped * If the resource is genuinely optional, set `allowMissing=true` on `@initKubernetes()` and `@required=false` on the item ### Permission denied (403) [Section titled “Permission denied (403)”](#permission-denied-403) * Check that the active identity has the required RBAC: `kubectl auth can-i get secrets -n ` * For in-cluster use, verify the pod’s `serviceAccountName` is set and bound to a `Role`/`ClusterRole` that grants `get` on `secrets`/`configmaps` * The error message includes the exact `Role` snippet you need to grant ### Authentication failed (401) [Section titled “Authentication failed (401)”](#authentication-failed-401) * **Local dev:** Run `kubectl config current-context` and verify it points to the right cluster; run `kubectl get secrets` to confirm your kubeconfig works * **Explicit token:** Verify the token isn’t expired or revoked. Service account tokens created from `kubernetes.io/service-account-token` Secrets are long-lived, but TokenRequest-issued tokens have shorter TTLs * **In-cluster:** Check the pod’s mounted service account token at `/var/run/secrets/kubernetes.io/serviceaccount/token` ### Connection refused or TLS errors [Section titled “Connection refused or TLS errors”](#connection-refused-or-tls-errors) * Verify the cluster API URL is reachable from your machine/pod * For clusters with self-signed certificates and explicit auth, set `skipTlsVerify=true` (development only) * If using `kubectl` works but the plugin doesn’t, your kubeconfig may rely on an [exec credential plugin](https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/) (e.g., `aws eks get-token`, `gke-gcloud-auth-plugin`, `kubelogin`). Ensure the helper binary is on your `$PATH` ### Wrong namespace [Section titled “Wrong namespace”](#wrong-namespace) * The plugin uses (in order): the explicit `namespace` argument, the current kubeconfig context’s namespace, the pod’s mounted SA namespace, or `default` * To force a specific namespace, pass it explicitly: `@initKubernetes(namespace=my-ns)` ## Resources [Section titled “Resources”](#resources) * [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) * [Kubernetes ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/) * [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) * [Service Account Tokens](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) * [`@kubernetes/client-node`](https://github.com/kubernetes-client/javascript) # macOS Keychain > Using the macOS Keychain as a secrets source with Varlock The built-in `keychain()` resolver lets you load secrets directly from the **macOS Keychain** using declarative instructions in your `.env` files. It communicates with the Keychain through Varlock’s native Swift daemon, which enforces biometric authentication (Touch ID) and per-session access control. Built-in support No plugin installation is required. `keychain()` is built into Varlock and available on macOS. Or use `varlock()` If you don’t already have secrets stored in your macOS Keychain, you may want to use the [`varlock()` resolver](/guides/local-encryption) instead. It provides the same biometric-gated, device-local encryption benefits without needing to manage Keychain items, and it works cross-platform. See the [local encryption guide](/guides/local-encryption/) for more details. ## Features [Section titled “Features”](#features) * **Built-in**: no plugin or extra dependency needed * **Biometric gating**: access is protected by Touch ID via Varlock’s native daemon * **Interactive picker**: use `keychain(prompt)` to browse and select items via a native dialog * **Auto-write-back**: prompt mode writes the resolved reference back to your config file * **Named or positional syntax** for quick or precise lookups * **Field selection**: extract specific fields from keychain items * **Multiple keychain support**: access the login, System, or custom keychains Caution `keychain()` is only supported on **macOS**. It will throw an error on other platforms. If your team includes non-Mac developers, consider wrapping keychain items with a fallback or using a cross-platform secrets source for shared configs. ## Getting started [Section titled “Getting started”](#getting-started) Rather than wiring up items individually, the easiest way to get started is by using `keychain(prompt)`. This will open a native picker dialog where you can select existing keychain items, or create new ones. After selection, Varlock will automatically write the resolved reference back into your config file for future use. ```env-spec ITEM=keychain(prompt) # Opens a native picker dialog ``` ## Import plaintext env files [Section titled “Import plaintext env files”](#import-plaintext-env-files) If you already have sensitive plaintext values in a local `.env` file, import them into macOS Keychain and replace the plaintext with stable `keychain(...)` references. The file to import is the first argument: ```sh varlock keychain import .env --profile jb ``` By default this **edits the file in place**: each sensitive plaintext value is replaced by its `keychain(...)` ref, so the secret no longer lives on disk. Comments and non-sensitive values are left untouched. To write the refs to a *different* file instead and leave the source as-is, pass `--write-to`: ```sh varlock keychain import .env --profile jb --write-to .env.jb ``` Import requires an existing `.env.schema` file for the input env file so Varlock knows which input variables are secrets and which are not. It only imports variables marked `@sensitive` in that schema and never prints secret values. Re-running is safe: values already converted to `keychain(...)` refs are skipped. By default, Varlock refuses to overwrite an existing Keychain item (or, with `--write-to`, an existing ref in the target file); pass `--force` to overwrite. The file you name must be one Varlock loads as part of your env setup. It resolves your normal env graph (from `package.json` `varlock.loadPath`, or the files in the current directory) to read sensitivity from your schema, the same way `varlock encrypt --file` works. An arbitrary file outside that setup can’t be imported. The value stored in Keychain is taken from the file you named specifically; an override of the same variable in another file (such as `.env.local`) does not change what gets imported. Generated refs use `service="varlock"` and account names like `::`. The project defaults to the current directory name and can be overridden: ```sh varlock keychain import .env --profile jb --project my-app ``` ## Set one secret manually [Section titled “Set one secret manually”](#set-one-secret-manually) To store one secret without putting the value in shell history, run `set` and enter the value at the masked prompt: ```sh varlock keychain set API_KEY --profile jb --write-to .env.jb ``` This stores the item under `service="varlock"` with account `::API_KEY`, then writes the matching `keychain(...)` ref when `--write-to` is provided. If you need to paste a multi-line secret, pipe it through stdin instead of passing it as a command-line argument: ```sh cat secret.txt | varlock keychain set PRIVATE_KEY --profile jb --write-to .env.jb ``` By default, `set` refuses to overwrite an existing Keychain item or env ref. Pass `--force` to replace both. ## Access management [Section titled “Access management”](#access-management) If VarlockEnclave cannot read an existing Keychain item, grant access without using `/usr/bin/security` directly: ```sh varlock keychain fix-access --account "my-app:jb:API_KEY" ``` `--service` defaults to `varlock`, but can be overridden for legacy or manually-created Keychain items: ```sh varlock keychain fix-access --service "com.company.api" --account "admin" ``` You can also fix every explicit `keychain(...)` ref in an env file: ```sh varlock keychain fix-access --path .env.jb ``` ## List Keychain items [Section titled “List Keychain items”](#list-keychain-items) To see which Keychain items are available, list them by service name. This shows metadata only (service, account, and keychain) and never reads secret values: ```sh varlock keychain list # same as bare `varlock keychain` varlock keychain list API_KEY # filter (matches service, account, or label) ``` Pass `--keychain` to search a specific keychain, such as `System`: ```sh varlock keychain list --keychain System ``` ## Reference [Section titled “Reference”](#reference) #### `keychain()` [Section titled “keychain()”](#keychain) Fetch a secret from the macOS Keychain. Communicates with the Keychain through Varlock’s native daemon, which enforces biometric (Touch ID) authentication. **Array args:** * `service` (optional): Service name of the keychain item (positional shorthand) * `prompt` (optional): Enter interactive picker mode **Key/value args:** * `service` (optional): Service name of the keychain item * `account` (optional): Account identifier for the keychain item * `keychain` (optional): Name of a specific keychain to search (e.g., `"System"`) * `field` (optional): Specific field to extract from the keychain item * `prompt` (optional): If set, opens a native picker dialog for interactive selection ```env-spec # Positional shorthand DATABASE_PASSWORD=keychain("com.company.database") # Named service param API_KEY=keychain(service="com.company.api") # With account ADMIN_PW=keychain("com.company.db", account="admin") # Targeting a specific keychain CERT=keychain("com.company.cert", keychain="System") # Field selection TOKEN=keychain("com.company.auth", field="password") # Interactive picker mode NEW_SECRET=keychain(prompt) ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### ”keychain() is only supported on macOS” [Section titled “”keychain() is only supported on macOS””](#keychain-is-only-supported-on-macos) This resolver requires macOS. It is not available on Linux or Windows. If you need cross-platform secret management, consider using one of the [plugin-based secret sources](/plugins/overview). ### Failed to read keychain item [Section titled “Failed to read keychain item”](#failed-to-read-keychain-item) * Verify the item exists in Keychain Access.app * Check that the service name and account match exactly * Try `keychain(prompt)` to browse available items and grant VarlockEnclave access via the native dialog ### Selection was cancelled [Section titled “Selection was cancelled”](#selection-was-cancelled) If you dismiss the native picker without selecting an item, the resolution will fail. Run `varlock` again to retry, or replace `keychain(prompt)` with an explicit `keychain(service="...")` reference. ### VarlockEnclave access [Section titled “VarlockEnclave access”](#varlockenclave-access) The first time you access a keychain item through Varlock, macOS may prompt you to grant access to VarlockEnclave. Approve this to allow future reads. If you accidentally denied access, you can update the item’s Access Control settings in Keychain Access.app. # Plugins Overview > Varlock plugins overview Plugins allow extending the functionality of Varlock. See the [plugins guide](/guides/plugins/) for more details on using plugins. For now, only official Varlock plugins under the `@varlock` npm scope are supported. We plan to support third-party plugins in the future, along with loading plugins from different sources (e.g., local files, git, npm/jsr, http, etc.). ## Official plugins [Section titled “Official plugins”](#official-plugins) | Plugin | npm package | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [1Password](/plugins/1password/) | [![](https://img.shields.io/npm/v/@varlock/1password-plugin?label=%40varlock%2F1password-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/1password-plugin) | | [Akeyless](/plugins/akeyless/) | [![](https://img.shields.io/npm/v/@varlock/akeyless-plugin?label=%40varlock%2Fakeyless-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/akeyless-plugin) | | [AWS](/plugins/aws-secrets/) (Secrets Manager & Parameter Store) | [![](https://img.shields.io/npm/v/@varlock/aws-secrets-plugin?label=%40varlock%2Faws-secrets-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/aws-secrets-plugin) | | [Azure Key Vault](/plugins/azure-key-vault/) | [![](https://img.shields.io/npm/v/@varlock/azure-key-vault-plugin?label=%40varlock%2Fazure-key-vault-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/azure-key-vault-plugin) | | [Bitwarden](/plugins/bitwarden/) | [![](https://img.shields.io/npm/v/@varlock/bitwarden-plugin?label=%40varlock%2Fbitwarden-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/bitwarden-plugin) | | [Dashlane](/plugins/dashlane/) | [![](https://img.shields.io/npm/v/@varlock/dashlane-plugin?label=%40varlock%2Fdashlane-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/dashlane-plugin) | | [Doppler](/plugins/doppler/) | [![](https://img.shields.io/npm/v/@varlock/doppler-plugin?label=%40varlock%2Fdoppler-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/doppler-plugin) | | [Google Secrets Manager](/plugins/google-secret-manager/) | [![](https://img.shields.io/npm/v/@varlock/google-secret-manager-plugin?label=%40varlock%2Fgoogle-secret-manager-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/google-secret-manager-plugin) | | [HashiCorp Vault](/plugins/hashicorp-vault/) (and OpenBao) | [![](https://img.shields.io/npm/v/@varlock/hashicorp-vault-plugin?label=%40varlock%2Fhashicorp-vault-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/hashicorp-vault-plugin) | | [Infisical](/plugins/infisical/) | [![](https://img.shields.io/npm/v/@varlock/infisical-plugin?label=%40varlock%2Finfisical-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/infisical-plugin) | | [KeePass](/plugins/keepass/) | [![](https://img.shields.io/npm/v/@varlock/keepass-plugin?label=%40varlock%2Fkeepass-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/keepass-plugin) | | [Keeper](/plugins/keeper/) | [![](https://img.shields.io/npm/v/@varlock/keeper-plugin?label=%40varlock%2Fkeeper-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/keeper-plugin) | | [Kubernetes](/plugins/kubernetes/) (Secrets & ConfigMaps) | [![](https://img.shields.io/npm/v/@varlock/kubernetes-plugin?label=%40varlock%2Fkubernetes-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/kubernetes-plugin) | | [macOS Keychain](/plugins/macos-keychain/) | N/A - built-in | | [Pass](/plugins/pass/) (unix password manager) | [![](https://img.shields.io/npm/v/@varlock/pass-plugin?label=%40varlock%2Fpass-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/pass-plugin) | | [Passbolt](/plugins/passbolt/) | [![](https://img.shields.io/npm/v/@varlock/passbolt-plugin?label=%40varlock%2Fpassbolt-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/passbolt-plugin) | | [Proton Pass](/plugins/proton-pass/) | [![](https://img.shields.io/npm/v/@varlock/proton-pass-plugin?label=%40varlock%2Fproton-pass-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/proton-pass-plugin) | Looking for something else? If you have a specific plugin in mind, please join us on [Discord](https://chat.dmno.dev) and let us know! # Pass Plugin > Using pass (the standard unix password manager) with Varlock [![](https://img.shields.io/npm/v/@varlock/pass-plugin?label=%40varlock%2Fpass-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/pass-plugin) Our [pass](https://www.passwordstore.org/) plugin enables loading secrets from `pass` (the standard unix password manager) using declarative instructions within your `.env` files. Pass stores each secret as a GPG-encrypted file in `~/.password-store`, organized in a simple directory hierarchy. This plugin shells out to the `pass` CLI, so it works with your existing GPG agent, git-backed stores, and all standard pass configuration. ## Features [Section titled “Features”](#features) * **Zero-config**: works with your existing pass store * **GPG-backed encryption**: uses pass’s native GPG security model * **Auto-infer entry paths** from variable names * **Bulk-load secrets** with `passBulk()` via `@setValuesBulk` * **Multiple store instances** for accessing different pass stores * **Name prefixing** for scoped entry access * **`allowMissing`** option for optional secrets * **In-session caching**: each entry is decrypted only once per resolution * **Helpful error messages** with resolution tips ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/pass-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/pass-plugin) # # 2. Initialize the plugin - no arguments needed for default setup # @initPass() ``` ### Prerequisites [Section titled “Prerequisites”](#prerequisites) You must have `pass` installed on your system: ```bash # macOS brew install pass # Ubuntu/Debian sudo apt-get install pass # Arch pacman -S pass ``` Your password store must already be initialized (`pass init "Your GPG Key ID"`). See the [pass documentation](https://www.passwordstore.org/) for setup details. Note The plugin does **not** fail at load time if `pass` is not installed. It only fails when you actually try to access a secret, making it safe to include in shared configs where some developers may not have pass set up. ### Custom store path [Section titled “Custom store path”](#custom-store-path) If your password store is in a non-standard location, use `storePath`: .env.schema ```env-spec # @plugin(@varlock/pass-plugin) # @initPass(storePath=/path/to/custom/store) ``` This sets `PASSWORD_STORE_DIR` for all pass commands issued by this plugin instance. ### Name prefixing [Section titled “Name prefixing”](#name-prefixing) Use `namePrefix` to scope all entry lookups under a common prefix: .env.schema ```env-spec # @plugin(@varlock/pass-plugin) # @initPass(namePrefix=production/app/) # --- # Fetches "production/app/DATABASE_PASSWORD" from the store DATABASE_PASSWORD=pass() # Fetches "production/app/stripe-key" STRIPE_KEY=pass("stripe-key") ``` ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) Access multiple different password stores (e.g., personal and team): .env.schema ```env-spec # @plugin(@varlock/pass-plugin) # @initPass(id=personal) # @initPass(id=team, storePath=/shared/team-store) # --- MY_TOKEN=pass(personal, "tokens/github") SHARED_KEY=pass(team, "api-keys/stripe") ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values using the `pass()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) Fetch secrets from your pass store: .env.schema ```env-spec # Entry path defaults to the variable name DATABASE_PASSWORD=pass() API_KEY=pass() # Or explicitly specify the entry path STRIPE_KEY=pass("services/stripe/live-key") # Nested entries DB_URL=pass("production/database/url") ``` When called without arguments, `pass()` uses the config item key as the entry path. ### Handling optional secrets [Section titled “Handling optional secrets”](#handling-optional-secrets) Use `allowMissing` when a secret may not exist in the store: .env.schema ```env-spec # Returns empty string instead of erroring if entry doesn't exist OPTIONAL_KEY=pass("monitoring/datadog-key", allowMissing=true) ``` ### Multiline entries [Section titled “Multiline entries”](#multiline-entries) By default, `pass()` returns only the **first line** of the entry (the password), matching pass’s own convention where the password lives on line 1 and metadata follows. This is the same behavior as `pass -c` (copy to clipboard). To retrieve the full multiline content, use `multiline=true`: .env.schema ```env-spec # Only returns the first line (the password) DB_PASSWORD=pass("production/database") # Returns all lines (password + metadata) DB_FULL_ENTRY=pass("production/database", multiline=true) ``` ### Bulk loading secrets [Section titled “Bulk loading secrets”](#bulk-loading-secrets) Use `passBulk()` with `@setValuesBulk` to fetch all entries under a directory in your pass store in one go, instead of wiring up each secret individually: .env.schema ```env-spec # @plugin(@varlock/pass-plugin) # @initPass() # @setValuesBulk(passBulk("services")) # --- # These will be populated from entries under services/ in the pass store STRIPE_KEY= DATABASE_URL= ``` `passBulk()` lists entries via `pass ls`, then fetches each one in parallel. Each entry returns the first line only (matching the `pass()` default). You can customize the scope: .env.schema ```env-spec # Load everything from the store root # @setValuesBulk(passBulk()) # Load from a specific subdirectory # @setValuesBulk(passBulk("production/api")) # With a named instance # @setValuesBulk(passBulk(team, "shared")) ``` *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initPass()` [Section titled “@initPass()”](#initpass) Initialize a pass plugin instance for accessing secrets from a password store. **Key/value args:** * `storePath` (optional): Custom password store path (overrides `PASSWORD_STORE_DIR`, defaults to `~/.password-store`) * `namePrefix` (optional): Prefix automatically prepended to all entry paths * `id` (optional): Instance identifier for multiple instances ```env-spec # Default setup # @initPass() # Custom store location # @initPass(storePath=/path/to/store) # With prefix and ID # @initPass(id=prod, namePrefix=production/) ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `pass()` [Section titled “pass()”](#pass) Fetch a secret from the pass store. Returns the first line of the entry (the password) by default, matching pass’s convention. **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `entryPath` (optional): path to the entry in the pass store. If omitted, uses the variable name. **Key/value args:** * `allowMissing` (optional): if `true`, returns empty string instead of erroring when the entry doesn’t exist * `multiline` (optional): if `true`, returns the full entry content instead of just the first line ```env-spec # Auto-infer entry path from variable name DATABASE_PASSWORD=pass() # Explicit entry path STRIPE_KEY=pass("services/stripe/live-key") # With instance ID TEAM_SECRET=pass(team, "shared/api-key") # Allow missing entries OPTIONAL=pass("maybe/exists", allowMissing=true) # Get full multiline content FULL_ENTRY=pass("services/config", multiline=true) ``` #### `passBulk()` [Section titled “passBulk()”](#passbulk) Fetch all entries under a directory in the pass store at once. Intended for use with `@setValuesBulk`. Lists entries via `pass ls`, then fetches each one in parallel. Each entry returns the first line only (matching the `pass()` default). **Array args:** * `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized * `pathPrefix` (optional): directory prefix to load entries from ```env-spec # Load all entries from the store root # @setValuesBulk(passBulk()) # Load entries under a specific path # @setValuesBulk(passBulk("services")) # With instance ID # @setValuesBulk(passBulk(team, "shared")) ``` *** ## Example configurations [Section titled “Example configurations”](#example-configurations) ### Simple development setup [Section titled “Simple development setup”](#simple-development-setup) .env.schema ```env-spec # @plugin(@varlock/pass-plugin) # @initPass() # --- # Entry paths match variable names DATABASE_URL=pass() REDIS_URL=pass() STRIPE_KEY=pass() ``` ### Production with path organization [Section titled “Production with path organization”](#production-with-path-organization) .env.schema ```env-spec # @plugin(@varlock/pass-plugin) # @initPass(namePrefix=production/) # --- # Fetches production/database/url, production/database/password, etc. DB_URL=pass("database/url") DB_PASSWORD=pass("database/password") STRIPE_KEY=pass("api/stripe-key") SENDGRID_KEY=pass("api/sendgrid-key") ``` ### Team and personal stores [Section titled “Team and personal stores”](#team-and-personal-stores) .env.schema ```env-spec # @plugin(@varlock/pass-plugin) # @initPass(id=personal) # @initPass(id=team, storePath=/shared/team-pass-store) # --- # Personal dev tokens GH_TOKEN=pass(personal, "tokens/github") # Shared team secrets SHARED_DB=pass(team, "databases/staging") SHARED_API_KEY=pass(team, "api-keys/internal") ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `pass` command not found [Section titled “pass command not found”](#pass-command-not-found) * Install pass using your system package manager (see [Prerequisites](#prerequisites)) * Ensure `pass` is in your `PATH` ### Entry not found [Section titled “Entry not found”](#entry-not-found) * Verify the entry exists: `pass show ` * List available entries: `pass ls` * Check for typos in the entry path * If using `namePrefix`, remember it’s prepended automatically ### GPG decryption failed [Section titled “GPG decryption failed”](#gpg-decryption-failed) * Ensure your GPG key is available: `gpg --list-keys` * Start the GPG agent: `gpgconf --launch gpg-agent` * You may need to enter your GPG passphrase ### Password store not initialized [Section titled “Password store not initialized”](#password-store-not-initialized) * Run `pass init "Your GPG Key ID"` to initialize the store * See `pass init --help` for details ## Resources [Section titled “Resources”](#resources) * [pass - The Standard Unix Password Manager](https://www.passwordstore.org/) * [pass man page](https://git.zx2c4.com/password-store/about/) * [GPG documentation](https://gnupg.org/documentation/) # Passbolt Plugin > Using Passbolt with Varlock [![](https://img.shields.io/npm/v/@varlock/passbolt-plugin?label=%40varlock%2Fpassbolt-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/passbolt-plugin) Our [Passbolt](https://www.passbolt.com/) plugin loads secrets from Passbolt password manager using declarative instructions within your `.env` files. Passbolt is an open-source, self-hosted password manager built for teams. This plugin connects to your Passbolt instance’s API using your account kit and passphrase, decrypting secrets via OpenPGP. ## Features [Section titled “Features”](#features) * **Account kit authentication**: provide your account kit and passphrase * **UUID-based secret access**: fetch secrets by their resource UUID * **`#field` syntax**: read specific fields (username, password, URI, TOTP, custom fields) * **Bulk loading** with `passboltBulk()` to load all passwords from a folder * **Custom fields object** with `passboltCustomFieldsObj()` for `@setValuesBulk` * **Self-hosted support**: API URL is read from your account kit * **Multiple instances**: connect to different Passbolt instances ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/passbolt-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/passbolt-plugin) # # 2. Initialize the plugin - see below for auth setup # @initPassbolt(accountKit=$PB_ACCOUNT_KIT, passphrase=$PB_PASSPHRASE) # --- # 3. Add config items for your credentials # @type=passboltAccountKit @sensitive @internal PB_ACCOUNT_KIT= # @sensitive PB_PASSPHRASE= ``` ### Authentication setup [Section titled “Authentication setup”](#authentication-setup) The plugin authenticates using your Passbolt **account kit** (a base64-encoded bundle containing your server URL, keys, and user info) and your **passphrase** (used to decrypt your private key). 1. **Download your account kit** from Passbolt: * Open your Passbolt instance * Navigate to **Manage account** → **Desktop app setup** * Click **Download your account kit** and copy the base64 contents 2. **Wire up your credentials in the schema**: .env.schema ```env-spec # @plugin(@varlock/passbolt-plugin) # @initPassbolt(accountKit=$PB_ACCOUNT_KIT, passphrase=$PB_PASSPHRASE) # --- # @type=passboltAccountKit @sensitive @internal PB_ACCOUNT_KIT= # @sensitive PB_PASSPHRASE= ``` 3. **Set your credentials in deployed environments**. Copy the account kit and passphrase values and set them using your platform’s env var management UI. ### Multiple instances [Section titled “Multiple instances”](#multiple-instances) If you need to connect to multiple Passbolt instances or users, register named instances using the `id` parameter: .env.schema ```env-spec # @initPassbolt(id=prod, accountKit=$PROD_ACCOUNT_KIT, passphrase=$PROD_PASSPHRASE) # @initPassbolt(id=dev, accountKit=$DEV_ACCOUNT_KIT, passphrase=$DEV_PASSPHRASE) ``` ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Once the plugin is installed and initialized, you can start adding config items that load values from Passbolt using the `passbolt()` resolver function. ### Basic usage [Section titled “Basic usage”](#basic-usage) Fetch secrets by resource UUID. By default, the password field is returned: .env.schema ```env-spec # Fetch password (default field) by resource UUID DB_PASSWORD=passbolt("01234567-0123-4567-890a-bcdef0123456") API_KEY=passbolt("76543210-3210-4321-a098-ba9876543210") ``` ### Fetching specific fields [Section titled “Fetching specific fields”](#fetching-specific-fields) Use `#field` syntax to fetch a specific field from a resource: .env.schema ```env-spec # Built-in fields LOGIN_URI=passbolt("01234567-0123-4567-890a-bcdef0123456#uri") LOGIN_USER=passbolt("01234567-0123-4567-890a-bcdef0123456#username") LOGIN_PASS=passbolt("01234567-0123-4567-890a-bcdef0123456#password") # TOTP fields TOTP_SECRET=passbolt("01234567-0123-4567-890a-bcdef0123456#totp.secret") TOTP_CODE=passbolt("01234567-0123-4567-890a-bcdef0123456#totp.code") # Custom fields: any unrecognized name is treated as a custom field MY_FIELD=passbolt("01234567-0123-4567-890a-bcdef0123456#MyCustomField") ``` Alternatively, use the named `field` parameter: .env.schema ```env-spec LOGIN_PASS=passbolt("01234567-0123-4567-890a-bcdef0123456", field="password") CUSTOM=passbolt("01234567-0123-4567-890a-bcdef0123456", field="AnotherField") ``` ### Using multiple instances [Section titled “Using multiple instances”](#using-multiple-instances) When multiple plugin instances are initialized, pass the instance ID as the first argument: .env.schema ```env-spec PROD_SECRET=passbolt(prod, "11111111-1111-4111-a111-111111111111") DEV_SECRET=passbolt(dev, "22222222-2222-4222-b222-222222222222") ``` ### Bulk loading from a folder [Section titled “Bulk loading from a folder”](#bulk-loading-from-a-folder) Use `passboltBulk()` with `@setValuesBulk` to load all passwords from a Passbolt folder at once. Resources are matched by name: .env.schema ```env-spec # @plugin(@varlock/passbolt-plugin) # @initPassbolt(accountKit=$PB_ACCOUNT_KIT, passphrase=$PB_PASSPHRASE) # @setValuesBulk(passboltBulk(folderPath="Database/Dev")) # --- # @type=passboltAccountKit @sensitive @internal PB_ACCOUNT_KIT= # @sensitive PB_PASSPHRASE= # These will be populated from Passbolt (matched by resource name) API_KEY= DB_PASSWORD= ``` ### Bulk loading custom fields [Section titled “Bulk loading custom fields”](#bulk-loading-custom-fields) Use `passboltCustomFieldsObj()` with `@setValuesBulk` to load all custom fields from a single resource: .env.schema ```env-spec # @setValuesBulk(passboltCustomFieldsObj("01234567-0123-4567-890a-bcdef0123456")) # --- # These will be populated from custom field names API_KEY= DB_PASSWORD= ``` ### Finding resource UUIDs [Section titled “Finding resource UUIDs”](#finding-resource-uuids) To find a resource’s UUID: 1. Open your Passbolt instance 2. Navigate to the resource 3. Copy the UUID from the URL (format: `xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx`) *** ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initPassbolt()` [Section titled “@initPassbolt()”](#initpassbolt) Initializes an instance of the Passbolt plugin, setting up authentication and options. Can be called multiple times for different instances. **Key/value args:** * `accountKit` **(required)**: Passbolt account kit (base64-encoded). Should be a reference to a config item of type `passboltAccountKit`. * `passphrase` **(required)**: Passphrase to decrypt your private key. Should be a reference to a sensitive config item. * `cacheTtl` (optional): cache resolved values for the specified duration (e.g. `"5m"`, `"1h"`, `"1d"`, or `forever` to cache until manually cleared). For cache mode behavior and CLI cache controls, see the [Caching guide](/guides/caching/). * `id` (optional): instance identifier for multiple instances ```env-spec # @initPassbolt(accountKit=$PB_ACCOUNT_KIT, passphrase=$PB_PASSPHRASE, cacheTtl="1h") # --- # @type=passboltAccountKit @sensitive @internal PB_ACCOUNT_KIT= # @sensitive PB_PASSPHRASE= ``` ### Data types [Section titled “Data types”](#data-types) #### `passboltAccountKit` [Section titled “passboltAccountKit”](#passboltaccountkit) This type is [`@internal`](/reference/item-decorators/#internal) by default: varlock uses it to fetch your other secrets but does **not** inject it into your application. Override with `@internal=false` if your app uses the credential directly, for example to write secrets back or fetch additional secrets at runtime. Represents a Passbolt account kit (base64-encoded). Validation ensures the value is valid base64. The type is marked as `@sensitive`, so adding an explicit `@sensitive` decorator is optional. ```env-spec # @type=passboltAccountKit @sensitive @internal PB_ACCOUNT_KIT= ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `passbolt()` [Section titled “passbolt()”](#passbolt) Fetches a field from a Passbolt resource. Returns the password field by default. **Array args:** * `instanceId` (optional): instance identifier when multiple plugin instances are initialized * `resourceId`: UUID of the resource to fetch. Supports `#field` suffix for field extraction. **Key/value args:** * `field` (optional): alternative to `#field` syntax for specifying which field to read **Built-in fields:** `password` (default), `username`, `uri`, `totp.secret`, `totp.code`. Any other name is treated as a custom field. ```env-spec # Default password field SECRET=passbolt("01234567-0123-4567-890a-bcdef0123456") # Specific field via #field USER=passbolt("01234567-0123-4567-890a-bcdef0123456#username") # Named field parameter USER=passbolt("01234567-0123-4567-890a-bcdef0123456", field="username") # With instance ID SECRET=passbolt(prod, "01234567-0123-4567-890a-bcdef0123456") ``` #### `passboltBulk()` [Section titled “passboltBulk()”](#passboltbulk) Loads all passwords from a Passbolt folder as a JSON map keyed by resource name. Intended for use with `@setValuesBulk`. **Array args:** * `instanceId` (optional): instance identifier when multiple plugin instances are initialized **Key/value args:** * `folderPath` **(required)**: folder path to load resources from. Use `/` to separate nested folders (escape literal `/` in names with `\`). ```env-spec # Load from a folder # @setValuesBulk(passboltBulk(folderPath="Production")) # Nested folder # @setValuesBulk(passboltBulk(folderPath="Production/Database")) # With instance ID # @setValuesBulk(passboltBulk(prod, folderPath="Production")) ``` #### `passboltCustomFieldsObj()` [Section titled “passboltCustomFieldsObj()”](#passboltcustomfieldsobj) Fetches all custom fields from a Passbolt resource as a JSON object. Intended for use with `@setValuesBulk`. **Array args:** * `instanceId` (optional): instance identifier when multiple plugin instances are initialized * `resourceId`: UUID of the resource ```env-spec # Load custom fields from a resource # @setValuesBulk(passboltCustomFieldsObj("01234567-0123-4567-890a-bcdef0123456")) # With instance ID # @setValuesBulk(passboltCustomFieldsObj(prod, "01234567-0123-4567-890a-bcdef0123456")) ``` *** ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Authentication failed [Section titled “Authentication failed”](#authentication-failed) * Verify the account kit is correct (must be valid base64) * Ensure the passphrase matches the one used when the account was created ### Resource not found [Section titled “Resource not found”](#resource-not-found) * Verify the resource UUID is correct (must be valid UUID v4 format) * Check that the resource exists in your Passbolt instance * Ensure the resource is shared with your user ### Folder not found [Section titled “Folder not found”](#folder-not-found) * Check that the folder path is correct and exists * Folder paths are case-sensitive * Use `/` to separate nested folders ## Resources [Section titled “Resources”](#resources) * [Passbolt](https://www.passbolt.com/): open-source password manager for teams * [Passbolt Help](https://help.passbolt.com/): official documentation * [Passbolt Community](https://community.passbolt.com/): community forums # Proton Pass Plugin > Using Proton Pass with Varlock [![](https://img.shields.io/npm/v/@varlock/proton-pass-plugin?label=%40varlock%2Fproton-pass-plugin\&color=9B55F5\&logo=npm)](https://npmx.dev/package/@varlock/proton-pass-plugin) Our [Proton Pass](https://proton.me/pass) plugin loads secrets from Proton Pass using declarative instructions within your `.env` files. It shells out to the official Proton Pass CLI (`pass-cli`) to resolve secret references in the format `pass://vault/item/field`. ## Features [Section titled “Features”](#features) * **Secret references via `pass://` URIs** (`pass://vault/item/field`) * **Personal access token login** for CI (`PROTON_PASS_PERSONAL_ACCESS_TOKEN`): non-interactive, scoped, no full account credentials * **Non-interactive password login** using environment variables (`PROTON_PASS_PASSWORD`, `PROTON_PASS_TOTP`, `PROTON_PASS_EXTRA_PASSWORD`) * **In-session caching** per resolution run * **Helpful error messages** with resolution tips ## Installation and setup [Section titled “Installation and setup”](#installation-and-setup) In a JS/TS project, you may install the `@varlock/proton-pass-plugin` package as a normal dependency. Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. ### Install Proton Pass CLI [Section titled “Install Proton Pass CLI”](#install-proton-pass-cli) You must have `pass-cli` installed: ```bash curl -fsSL https://proton.me/download/pass-cli/install.sh | bash ``` See: [Proton Pass CLI overview](https://protonpass.github.io/pass-cli/). ### Configure the plugin [Section titled “Configure the plugin”](#configure-the-plugin) In production/CI you typically want a fully non-interactive login. The plugin supports two authentication methods: a **personal access token** (recommended) or a **username + password**. Login credentials are `@internal` by default The login credential types (`protonPassPersonalAccessToken`, `protonPassPassword`, `protonPassTotp`, `protonPassExtraPassword`) are marked [`@internal`](/reference/item-decorators/#internal): varlock uses them to authenticate but does **not** inject them into your application. Override with `@internal=false` if needed. #### Personal access token (recommended) [Section titled “Personal access token (recommended)”](#personal-access-token-recommended) A [personal access token](https://protonpass.github.io/pass-cli/commands/login/#personal-access-token-login) is a scoped, non-interactive credential. It never needs a username, password, or TOTP, which makes it the cleanest option for CI. Create one with the Proton Pass CLI, then grant it access to the vaults/items it needs: ```bash # Create a token (the full value is only shown once, so save it somewhere safe) pass-cli pat create --name "deploy-bot" --expiration 3m # PROTON_PASS_PERSONAL_ACCESS_TOKEN=pst_xxxx...xxxx::TOKENKEY # Grant it access to a vault or item pass-cli pat access grant ... ``` .env.schema ```env-spec # 1. Load the plugin # @plugin(@varlock/proton-pass-plugin) # # 2. Initialize Proton Pass plugin instance # @initProtonPass( # id=prod, # personalAccessToken=$PROTON_PASS_PERSONAL_ACCESS_TOKEN # ) # --- # @type=protonPassPersonalAccessToken @sensitive @internal PROTON_PASS_PERSONAL_ACCESS_TOKEN= ``` When a `personalAccessToken` is provided it takes precedence: the plugin runs `pass-cli login` with the token and ignores `username`/`password`/`totp`/`extraPassword`. Token expiration Personal access tokens have a **mandatory expiration** set when you create them (`1d`, `1w`, `1m`, `3m`, `6m`, or `1y`) and do **not** auto-renew. When a token expires, mint a new one with `pass-cli pat renew` (or `pat create`) and update the value in your config. There is nothing for the plugin to refresh or cache on your behalf. #### Username + password [Section titled “Username + password”](#username--password) .env.schema ```env-spec # @initProtonPass( # id=prod, # username=$PROTON_PASS_USERNAME, # password=$PROTON_PASS_PASSWORD, # totp=$PROTON_PASS_TOTP, # extraPassword=$PROTON_PASS_EXTRA_PASSWORD # ) # --- # @type=protonPassPassword @sensitive @internal PROTON_PASS_PASSWORD= # @type=protonPassTotp @sensitive @internal PROTON_PASS_TOTP= # @type=protonPassExtraPassword @sensitive @internal PROTON_PASS_EXTRA_PASSWORD= ``` The `username` is passed as an argument to `pass-cli login --interactive `. The password/TOTP/extra password are provided to the CLI via environment variables, as supported by Proton Pass CLI login docs: [login command](https://protonpass.github.io/pass-cli/commands/login/). Already logged in? If `pass-cli` is already logged in, you can also omit credentials and just rely on the existing session. This mostly applies to local development, in CI, you will probably want static credentials. ### Reducing local login prompts [Section titled “Reducing local login prompts”](#reducing-local-login-prompts) For local development, especially with multi-process task runners like Turbo, these patterns help reduce repeated prompts: * Store Proton Pass credentials in local config items (for example `.env.local`), wire them into `@initProtonPass(...)`, and encrypt sensitive values using [Varlock local encryption](/guides/local-encryption/). * Reuse an existing `pass-cli` session when possible (`pass-cli login` once in your terminal before starting your dev tasks). TOTP in local config If your account requires TOTP, a static value in config will expire quickly. For fully non-interactive workflows, use a [personal access token](#personal-access-token-recommended) instead. ## Loading secrets [Section titled “Loading secrets”](#loading-secrets) Use the `protonPass()` resolver function to fetch a field value from a Proton Pass secret reference. Secret reference syntax (as documented by Proton Pass CLI): [secret references](https://protonpass.github.io/pass-cli/commands/contents/secret-references/). .env.schema ```env-spec # Fetch the `password` field from a secret reference: DB_PASSWORD=protonPass(pass://Production/Database/password) ``` When you need multiple plugin instances, use the optional first argument to select the `id`: .env.schema ```env-spec DB_PASSWORD=protonPass(prod, pass://Production/Database/password) ``` ### Optional secrets [Section titled “Optional secrets”](#optional-secrets) If a secret might not exist, pass `allowMissing=true`: .env.schema ```env-spec OPTIONAL_DB_PASSWORD=protonPass(pass://Production/Database/password, allowMissing=true) ``` When allowed, missing secrets resolve to an empty string (`""`). ## Reference [Section titled “Reference”](#reference) ### Root decorators [Section titled “Root decorators”](#root-decorators) #### `@initProtonPass()` [Section titled “@initProtonPass()”](#initprotonpass) Initialize a Proton Pass plugin instance for `protonPass()` resolver. **Key/value args:** * `id` (optional): instance identifier for multiple instances * `personalAccessToken` (optional): personal access token (maps to `PROTON_PASS_PERSONAL_ACCESS_TOKEN`). When set, takes precedence over username/password. * `username` (optional): login username/email passed to `pass-cli login --interactive` * `password` (optional): `pass-cli` password (maps to `PROTON_PASS_PASSWORD`) * `totp` (optional): `pass-cli` TOTP code (maps to `PROTON_PASS_TOTP`) * `extraPassword` (optional): `pass-cli` extra password (maps to `PROTON_PASS_EXTRA_PASSWORD`) ```env-spec # @initProtonPass( # id=prod, # personalAccessToken=$PROTON_PASS_PERSONAL_ACCESS_TOKEN # ) ``` ### Resolver functions [Section titled “Resolver functions”](#resolver-functions) #### `protonPass()` [Section titled “protonPass()”](#protonpass) Fetch a field value from Proton Pass using a secret reference. **Array args:** * `secretRef` (required): `pass://vault/item/field` * `instanceId` (optional, if 2 args are provided): instance identifier **Key/value args:** * `allowMissing` (optional): if `true`, returns empty string when the secret is missing ```env-spec # With secret reference (default instance) DB_PASSWORD=protonPass(pass://Production/Database/password) # With explicit instance DB_PASSWORD=protonPass(prod, pass://Production/Database/password) ``` ### Under the hood [Section titled “Under the hood”](#under-the-hood) This resolver uses: * `pass-cli item view --output json ` to fetch the requested field * `pass-cli login` only when an auth error occurs, then retries the original command. With a `personalAccessToken` it runs `pass-cli login` with `PROTON_PASS_PERSONAL_ACCESS_TOKEN` set; otherwise it runs `pass-cli login --interactive ` with the password env vars. # Builtin variables > Auto-detected VARLOCK_* variables for CI platform, branch, commit, and environment info Varlock provides a set of **builtin `VARLOCK_*` variables** that are automatically populated with information about the current CI/deploy platform, git branch, commit, and inferred deployment environment. They are entirely **opt-in**: they only exist in your schema when you reference them. ## Usage [Section titled “Usage”](#usage) Builtin variables are activated when you reference them via `$VARLOCK_*` in a value expression: .env.schema ```env-spec # @currentEnv=$VARLOCK_ENV # --- BUILD_TAG="build-$VARLOCK_COMMIT_SHA_SHORT" DB_URL=if( eq($VARLOCK_ENV, development), postgres://localhost/myapp, postgres://${VARLOCK_ENV}-db.example.com/myapp ) ``` If you want to include a builtin variable in your resolved env without referencing it from another item, define it with an empty value and varlock will populate it automatically: .env.schema ```env-spec VARLOCK_BRANCH= VARLOCK_COMMIT_SHA_SHORT= ``` You can also use `VARLOCK_ENV` as your environment flag with `@currentEnv`, which means you don’t need to create your own `APP_ENV` variable. Varlock will auto-detect the environment for you. Verify detection works for your setup Auto-detection is based on environment variables set by each CI/deploy platform. Different platforms expose different information, and detection heuristics (especially branch-to-environment inference) may not match your conventions. **Always verify that the detected values match your expectations** before relying on them in production. You can check the resolved values using `varlock run -- env | grep VARLOCK_` or by inspecting the output of `varlock load`. ## Builtin Vars [Section titled “Builtin Vars”](#builtin-vars) ### `VARLOCK_ENV` [Section titled “VARLOCK\_ENV”](#varlock_env) **Type:** `string`, one of `development`, `preview`, `staging`, `production`, `test` The inferred deployment environment. Detection follows this priority: 1. **Test environment**: detected from `NODE_ENV=test`, `VITEST`, `JEST_WORKER_ID`, or `VITEST_POOL_ID` 2. **Platform-provided**: uses the platform’s own environment concept (e.g., Vercel’s `VERCEL_ENV`, Netlify’s `CONTEXT`) 3. **Branch inference**: in CI, infers from branch name: `main`/`master`/`production`/`prod` → `production`, `staging`/`stage`/`develop`/`dev` → `staging`, `qa`/`test` → `test`, anything else → `preview` 4. **CI fallback**: if in CI but no branch info is available, defaults to `preview` 5. **Local fallback**: if not in CI, defaults to `development` #### Using with `@currentEnv` [Section titled “Using with @currentEnv”](#using-with-currentenv) .env.schema ```env-spec # @currentEnv=$VARLOCK_ENV # --- DB_HOST=if(forEnv(production), "prod-db.example.com", "localhost") DB_NAME=myapp DB_URL="postgres://$DB_HOST/$DB_NAME" ``` #### Test environment caveat [Section titled “Test environment caveat”](#test-environment-caveat) Test runners and `VARLOCK_ENV` Many test runners (Vitest, Jest, etc.) set `NODE_ENV=test` **after** the process has started, often after varlock has already loaded and resolved your env vars. This means `VARLOCK_ENV` may not detect `test` automatically in all setups. If you depend on `VARLOCK_ENV=test` to load `.env.test` or toggle behavior via `forEnv(test)`, **explicitly pass it** when running tests: ```bash VARLOCK_ENV=test bun run test # or VARLOCK_ENV=test varlock run -- vitest ``` This is the same pattern recommended for any environment flag. See the [environments guide](/guides/environments/) for more details. ### `VARLOCK_IS_CI` [Section titled “VARLOCK\_IS\_CI”](#varlock_is_ci) **Type:** `boolean` Whether the current process is running in a CI environment. ### `VARLOCK_BRANCH` [Section titled “VARLOCK\_BRANCH”](#varlock_branch) **Type:** `string | undefined` The current git branch name. In CI environments, sourced from the platform’s environment variables. When running locally (non-CI), auto-detected via `git branch --show-current`. Undefined if the branch cannot be determined (e.g., detached HEAD state, no git repo, or platform doesn’t expose branch info). ### `VARLOCK_PR_NUMBER` [Section titled “VARLOCK\_PR\_NUMBER”](#varlock_pr_number) **Type:** `string | undefined` The pull/merge request number, if the current build is for a PR. Undefined otherwise. ### `VARLOCK_COMMIT_SHA` [Section titled “VARLOCK\_COMMIT\_SHA”](#varlock_commit_sha) **Type:** `string | undefined` The full git commit SHA. ### `VARLOCK_COMMIT_SHA_SHORT` [Section titled “VARLOCK\_COMMIT\_SHA\_SHORT”](#varlock_commit_sha_short) **Type:** `string | undefined` The short (7-character) git commit SHA. ### `VARLOCK_PLATFORM` [Section titled “VARLOCK\_PLATFORM”](#varlock_platform) **Type:** `string | undefined` The name of the detected CI/deploy platform (e.g., `"GitHub Actions"`, `"Vercel"`, `"Netlify CI"`). ### `VARLOCK_BUILD_URL` [Section titled “VARLOCK\_BUILD\_URL”](#varlock_build_url) **Type:** `url | undefined` A URL linking to the current build or deploy in the CI platform’s UI. ### `VARLOCK_REPO` [Section titled “VARLOCK\_REPO”](#varlock_repo) **Type:** `string | undefined` The repository name in `owner/repo` format. ## Supported platforms [Section titled “Supported platforms”](#supported-platforms) Detection is built-in for these platforms (no configuration required): * GitHub Actions * GitLab CI * Vercel * Netlify * Cloudflare Pages / Workers * AWS CodeBuild * Azure Pipelines * Bitbucket Pipelines * Buildkite * CircleCI * Jenkins * Render * Travis CI * and [many more](https://github.com/dmno-dev/varlock/tree/main/packages/ci-env-info/src/platforms.ts) Not all platforms expose all fields. For example, some may not provide branch name or PR number. CI/deploy platform detection is powered by [`@varlock/ci-env-info`](https://npmx.dev/package/@varlock/ci-env-info), which can also be used as a standalone package. # CLI Commands > Reference documentation for Varlock CLI commands Varlock provides a command-line interface for managing environment variables and secrets. This reference documents all available CLI commands. See [installation](/getting-started/installation) for instructions on how to install Varlock. You can also enable [shell completion](/guides/shell-completion/) for tab completion of commands and flags. ### Running commands in JS projects [Section titled “Running commands in JS projects”](#running-commands-in-js-projects) If you have installed varlock as a `package.json` dependency, rather than a standalone binary, the best way to invoke the CLI is via your package manager: * npm ```bash npm exec -- varlock ... ``` * pnpm ```bash pnpm exec -- varlock ... ``` * bun ```bash bunx varlock ... ``` * vlt ```bash vlx -- varlock ... ``` * yarn ```bash yarn exec -- varlock ... ``` Also note that within package.json scripts, you can use it directly: package.json ```json { "scripts": { "start": "varlock run -- node app.js" } } ``` ### `package.json` configuration [Section titled “package.json configuration”](#packagejson-configuration) You can configure varlock’s default behavior by adding a `varlock` key to your `package.json`: package.json ```json { "varlock": { "loadPath": "./envs/" } } ``` | Option | Description | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `loadPath` | Path (or array of paths) to a directory or specific `.env` file to use as the default entry point. Defaults to the current working directory if not set. Use a **directory path** (with trailing `/`) to automatically load all relevant files (`.env.schema`, `.env`, `.env.local`, etc.); a file path only loads that file and its explicit imports. When an array is provided, all paths are loaded and combined, with later entries taking higher precedence. Can be overridden by the `--path` CLI flag. Varlock looks for this config in the `package.json` in the current working directory only. | Plugins While plugins cannot add additional CLI commands, they can extend varlock with additional [resolver functions](/reference/functions/), [data types](/reference/data-types/), and [decorators](/reference/item-decorators/). See the [Plugins guide](/guides/plugins/) for more information. ## Common options [Section titled “Common options”](#common-options) Several commands share these flags. They behave identically wherever they appear, except where a command’s own section calls out a deviation. * `--path` / `-p` ``: Path to a specific `.env` file or directory to use as the entry point (overrides `varlock.loadPath` in `package.json`). Can be specified multiple times to load from multiple paths, where later paths take higher precedence. *(For [`scan`](#scan) and [`audit`](#audit) this is the **schema** entry point used to resolve sensitive values; `audit` accepts a single path only.)* * `--env `: Resolve in the context of a specific environment (e.g., `--env production`). Overridden by `@currentEnv` if it is set in your `.env.schema`. * `--clear-cache`: Clear the active cache store before resolving values, then re-resolve all values (when combined with `--skip-cache`, the cache is cleared first, then reads and writes are skipped for the run). * `--skip-cache`: Skip cache entirely for this invocation (no reads or writes). This overrides `@cache=disk`/`@cache=memory`. ## Commands reference [Section titled “Commands reference”](#commands-reference) ### `varlock init` [Section titled “varlock init”](#init) Starts an interactive onboarding process to help you get started. Will help create your `.env.schema` and install varlock as a dependency if necessary. ```bash varlock init [options] ``` **Options:** * `--agent`: Run non-interactively for agent/automation workflows. Skips confirmation prompts and uses deterministic defaults for schema generation. **Examples:** ```bash # Interactive setup wizard varlock init # Non-interactive setup for AI agents varlock init --agent ``` Tip Install the Varlock agent skill with `npx skills add dmno-dev/varlock` or `gh skill install dmno-dev/varlock varlock`. See the [AI Tools guide](/guides/ai-tools/#install-the-varlock-skill) for more. ### `varlock load` [Section titled “varlock load”](#load) Loads and validates environment variables according to your .env files, and prints the results. Default prints a nicely formatted, colorized summary of the results, but can also print out machine-readable formats. Useful for debugging locally, and in CI to print out a summary of env vars. ```bash varlock load [options] ``` **Options:** * `--format`: Format of output \[pretty|json|env|shell|json-full] * `--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:** ```bash # Load and validate environment variables varlock load # Load and validate for a specific environment (when not using @currentEnv in .env.schema) varlock load --env production # Output validation results in JSON format varlock load --format json # Output full serialized graph (including errors/configErrors fields) varlock load --format json-full # Compact output varlock load --format json-full --compact # Output as shell export statements (useful for direnv / eval) eval "$(varlock load --format shell)" # When validation is failing, will show all items, rather than just failing ones varlock load --show-all # Load from a specific .env file varlock load --path .env.prod # Load from a specific directory varlock load --path ./config/ # Load from multiple directories (later paths take higher precedence) varlock load -p ./envs -p ./overrides # Agent-safe JSON output with sensitive values redacted varlock load --agent ``` **Exit codes:** * `0` when all config is valid * non-zero when validation fails (missing required values, failed coercion, or resolver errors) **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. [`@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. Caution Setting `@currentEnv` in your `.env.schema` will override the `--env` flag. ### `varlock run` [Section titled “varlock run”](#run) Executes a command in a child process, injecting your resolved and validated environment variables from your .env files. This is useful when a code-level integration is not possible. ```bash varlock run -- ``` **Exit codes:** `varlock run` exits with the child command’s exit code, so it is transparent in scripts and CI. If varlock itself fails before the command starts (e.g. invalid config or a failed resolver), it exits non-zero without running the command. **Options:** * `--redact-stdout` / `--no-redact-stdout`: Override automatic stdout/stderr redaction. `--redact-stdout` forces redaction of piped/redirected output (e.g., to override `@redactLogs=false`) and errors if output is attached to an interactive terminal, where redaction is not possible without breaking TTY behavior. `--no-redact-stdout` disables redaction entirely. * `--inject ` / `-i`: Control what gets injected into the child process environment: `all` (default: individual vars plus the `__VARLOCK_ENV` serialized config graph blob), `vars` (individual vars only, no blob), or `blob` (only the `__VARLOCK_ENV` blob, no individual vars) * `--include-internal`: Pass [`@internal`](/reference/item-decorators/#internal) items through to the child process. By default they are stripped from the child env, even if set in the ambient environment, so a secret-zero token never reaches your app. Use this for a *nested* `varlock run` whose own resolution needs the internal value. * [Common options](#common-options): `--path` / `-p`, `--clear-cache`, `--skip-cache` **Examples:** ```bash varlock run -- node app.js # Run a Node.js application varlock run -- python script.py # Run a Python script # Use a specific .env file as entry point varlock run --path .env.prod -- node app.js # Use a specific directory as entry point varlock run --path ./config/ -- node app.js # Use multiple directories as entry points varlock run -p ./envs -p ./overrides -- node app.js ``` Shell expansion of env vars in commands Because of the way that shell expansion works, you may need to use use `sh -c` to properly expand environment variables in your command *after* varlock has injected them. ```bash varlock run -- echo $MY_VAR # ❌ will not work varlock run -- sh -c 'echo $MY_VAR' # ✅ will work ``` Interactive tools and TTY detection `varlock run` automatically detects where each output stream is going: * **Interactive terminal (TTY)**: output passes straight through, preserving raw TTY behavior. Interactive tools like `psql` and `claude` work with no flags needed. A human at the terminal already has access to the secrets, so redaction adds little there. * **Piped or redirected output** (CI logs, files, `| tee`, etc.): output is piped through a redaction filter, since that’s where leaked secrets would persist. Detection is per stream. For example `varlock run -- node app.js | tee log.txt` redacts stdout (piped) while stderr (still attached to your terminal) passes through. To override the auto-detection: ```bash # force-disable redaction even when output is piped/redirected varlock run --no-redact-stdout -- node app.js | tee log.txt # force redaction of piped output even when @redactLogs=false is set varlock run --redact-stdout -- node app.js > log.txt ``` Redacting a TTY-attached stream is not possible without piping it (which would break interactive tools), so `--redact-stdout` errors if output is attached to an interactive terminal rather than silently degrading the experience. The same override can be set via the [`_VARLOCK_REDACT_STDOUT`](/reference/reserved-variables/#_varlock_redact_stdout) environment variable (`true`/`1` to force on, `false`/`0` to force off), which is handy when you can’t easily change the command being run, for example in a wrapper script or CI config. The CLI flag always takes precedence over the env var. Preventing sensitive value exposure via env inspection By default, `varlock run` injects a `__VARLOCK_ENV` variable containing the full serialized config graph (including resolved sensitive values) into the child process environment. This is used by framework integrations to obtain sensitivity metadata without re-invoking the CLI. For long-lived processes, interactive shells, or any workflow where subprocesses may inspect their environment (e.g., `env`, `printenv`, or an LLM-driven agent), use `--inject vars` to inject only the individual vars and omit this blob: ```bash varlock run --inject vars -- bash varlock run --inject vars -- claude ``` Note: When the `__VARLOCK_ENV` blob is omitted, framework integrations that rely on it will need to fall back to re-invoking the CLI. `__VARLOCK_RUN=1` is always set regardless of this flag. Signals and exit codes `varlock run` stays resident while your command runs, but it gets out of the way of process termination: * Terminating signals (`SIGTERM`, `SIGINT`, `SIGHUP`, `SIGQUIT`) are **forwarded** to the child so it can run its own shutdown handlers, rather than being killed abruptly. When no terminal is attached (containers, CI, background/agent runs), the child runs in its own process group and the signal is forwarded to the whole group, so grandchildren are terminated too. * The child’s exit status is **propagated faithfully**: a normal exit code passes through unchanged, and a child terminated by signal N exits with `128+N` (e.g. `143` for `SIGTERM`), matching shell conventions. This makes `varlock run` safe to use as a container `ENTRYPOINT` (including PID 1), so `docker stop` / pod termination reaches your app gracefully instead of waiting out the grace period and being `SIGKILL`ed. By default varlock forwards and then waits indefinitely for the child. It does **not** impose its own kill deadline, leaving that decision to the orchestrator or operator (a forwarded signal isn’t always terminal, since many programs use `SIGHUP` to reload). If you want varlock to escalate to `SIGKILL` when the child hasn’t exited within a grace period, set the `_VARLOCK_FORCE_KILL_TIMEOUT_MS` environment variable to a number of milliseconds. ### `varlock printenv` [Section titled “varlock printenv”](#printenv) Resolves and prints the value of a single environment variable to stdout. Only the requested item and its transitive dependencies are resolved, making this faster than loading the full graph. This is useful within larger shell commands where you need to embed a single resolved env var value. **Exit codes:** `0` when the value resolves; non-zero when no variable name is given, the variable is not in the schema, or it fails to resolve. ```bash varlock printenv [options] ``` **Positional arguments:** * ``: The variable to print. Required; printenv errors if omitted. **Options:** * [Common options](#common-options): `--path` / `-p`, `--clear-cache`, `--skip-cache` **Examples:** ```bash # Print the resolved value of MY_VAR varlock printenv MY_VAR # Use a specific .env file as entry point varlock printenv --path .env.prod MY_VAR # Use multiple directories as entry points varlock printenv -p ./envs -p ./overrides MY_VAR # Embed in a shell command using subshell expansion sh -c 'some-tool --token $(varlock printenv MY_TOKEN)' ``` Why not use `varlock run -- echo $MY_VAR`? Shell expansion happens *before* varlock runs, so `$MY_VAR` is substituted by the shell with whatever value it already has (likely empty). `varlock printenv` avoids this by printing the value directly to stdout, letting you capture it with `$(...)` *after* varlock has resolved it. ```bash varlock run -- echo $MY_VAR # ❌ shell expands $MY_VAR before varlock runs varlock printenv MY_VAR # ✅ varlock resolves and prints the value sh -c 'echo $(varlock printenv MY_VAR)' # ✅ embed in a larger command ``` ### `varlock explain` [Section titled “varlock explain”](#explain) Shows detailed information about how a single config item is resolved: all of its definitions, sources, overrides, and the final resolved value. This is the go-to command for debugging *why* a value is what it is (and an agent-friendly way to inspect resolution without dumping every value). Sensitive values are redacted in the output. **Exit codes:** `0` when the item is found and explained; non-zero when no key is given or the key is not in the schema. ```bash varlock explain [options] ``` **Positional arguments:** * ``: The config item to explain. Required; explain errors if omitted. **Options:** * [Common options](#common-options): `--env`, `--path` / `-p` **Examples:** ```bash # Explain how a value is resolved varlock explain DATABASE_URL # Explain in the context of a specific environment varlock explain --env production API_KEY ``` ### `varlock scan` [Section titled “varlock scan”](#scan) Scans your project files for sensitive config values that should not appear in plaintext. Loads your varlock config, resolves all `@sensitive` values, then checks files for any occurrences of those values. This is especially useful as a **pre-commit git hook** to prevent accidentally committing secrets into version control, and for **scanning build output** to ensure no secrets leaked into files that will be published or deployed. **Exit codes:** `0` when no plaintext secrets are found; `1` when a leaked value is detected. This makes it usable directly as a pre-commit hook or CI gate. ```bash varlock scan [paths...] [options] ``` **Positional arguments:** * `[paths...]`: Optional list of file paths, directories, or glob patterns to scan. When provided, only these targets are scanned: git filtering (`--staged`, `--include-ignored`) is bypassed and build-output directories that are normally skipped (such as `dist`, `.next`, `build`) are included. **Options:** * `--staged`: Only scan staged git files (ignored when explicit paths are provided) * `--include-ignored`: Include git-ignored files in the scan (ignored when explicit paths are provided) * `--install-hook`: Set up `varlock scan` as a git pre-commit hook * [Common options](#common-options): `--path` / `-p` (here it sets the **schema** entry point used to resolve sensitive values) **Examples:** ```bash # Scan all non-gitignored files in the current directory varlock scan # Only scan staged git files varlock scan --staged # Scan all files, including gitignored ones varlock scan --include-ignored # Scan a specific build output directory (e.g. to check for leaked secrets before publishing) varlock scan ./dist # Scan multiple directories varlock scan ./dist ./public # Scan files matching a glob pattern varlock scan './dist/**/*.js' # Use a specific .env file as the schema entry point varlock scan --path .env.prod # Use multiple schema entry points varlock scan -p ./envs -p ./overrides # Set up as a git pre-commit hook varlock scan --install-hook ``` Git pre-commit hook The easiest way to set up scanning as a pre-commit hook is to run: ```bash varlock scan --install-hook ``` This will detect if you are using a hook manager (like husky or lefthook) and provide the appropriate setup instructions. Otherwise, it will create a `.git/hooks/pre-commit` hook for you automatically. If varlock is installed as a project dependency, the hook command will be automatically prefixed with your package manager (e.g., `npx varlock scan`). You can also set it up manually. See the [Secrets guide](/guides/secrets/#scanning-for-leaked-secrets) for more details. ### `varlock encrypt` [Section titled “varlock encrypt”](#encrypt) Encrypts sensitive values using device-local encryption. Encrypted values are stored in `.env` files using the `varlock()` resolver function and are automatically decrypted at load time. On macOS, encryption is hardware-backed via the Secure Enclave (with Touch ID / biometric authentication). On Windows, keys are TPM-sealed when a TPM is available (Windows Hello gates interactive decrypts). On Linux, TPM2 and/or Secret Service is used. A pure-JavaScript file-based fallback is available on all platforms. See the [local encryption guide](/guides/local-encryption/) for platform details. On Windows, existing DPAPI keys are automatically upgraded to TPM sealing on the next decrypt. ```bash varlock encrypt [options] ``` **Options:** * `--file`: Path to a `.env` file; encrypts all sensitive plaintext values in-place **Examples:** ```bash # Interactive mode: encrypt a single value (prompts with hidden input) varlock encrypt # Pipe a value via stdin (keeps secrets out of shell history) printf '%s' "$SECRET" | varlock encrypt varlock encrypt < secret.txt # Encrypt all sensitive plaintext values in a .env file varlock encrypt --file .env.local ``` In single-value mode, you’ll either be prompted to enter a value (hidden input) or the value will be read from stdin when piped. The encrypted output is printed for you to copy into your `.env.local` file: ```plaintext SOME_SENSITIVE_KEY=varlock("local:") ``` Tip Piping via stdin avoids exposing secrets in your shell history. Prefer `printf '%s'` over `echo` to avoid a trailing newline (varlock strips a single trailing newline either way). In file mode, varlock loads the env graph, identifies `@sensitive` items with plaintext values, and lets you select which to encrypt in-place. Tip Use `varlock encrypt --file .env.local` after adding new secrets to quickly encrypt them all at once. Alternative: prompt mode Instead of encrypting values ahead of time, you can use `varlock(prompt)` as a placeholder in your `.env` files. On first load, varlock will prompt you to enter the secret and automatically replace the placeholder with the encrypted value. See the [`varlock()` function reference](/reference/functions/#varlock) for details. ### `varlock reveal` [Section titled “varlock reveal”](#reveal) Securely view or copy the value of a `@sensitive` environment variable. The value is displayed in an alternate terminal screen buffer so it doesn’t persist in your scrollback history. 🔒 Usually sensitive values are redacted, so this is needed to actually view the value without exposing it in plaintext on disk or in your terminal history. ```bash varlock reveal [VAR_NAME] [options] ``` **Options:** * `--copy`: Copy the value to clipboard instead of displaying (auto-clears after 10s) * [Common options](#common-options): `--path` / `-p`, `--env` **Examples:** ```bash # Interactive picker to browse and reveal sensitive values varlock reveal # Reveal a specific variable varlock reveal MY_SECRET # Copy a value to clipboard (auto-clears after 10s) varlock reveal MY_SECRET --copy ``` Note Non-sensitive values are not shown by `varlock reveal`. Use [`varlock printenv`](#printenv) for non-sensitive values, or to inject a sensitive value into a command. Clipboard support on Linux `varlock reveal --copy` uses your system clipboard command (`xclip` or `xsel`) on Linux. Install one of them if copy mode is unavailable. ### `varlock lock` [Section titled “varlock lock”](#lock) Locks the encryption daemon, requiring biometric authentication (e.g., Touch ID) for the next decrypt operation. This invalidates the current biometric session cache. ```bash varlock lock ``` This command only has an effect when using a biometric-enabled encryption backend (macOS Secure Enclave, Windows Hello, or Linux with polkit/PAM biometric setup). On other backends, it will display a message and exit. Tip Use `varlock lock` when stepping away from your machine to ensure the next person to decrypt a secret must authenticate biometrically. ### `varlock audit` [Section titled “varlock audit”](#audit) Scans your source code for environment variable references and compares them against keys defined in your schema. This command reports two drift categories: * **Missing in schema**: key is used in code but not declared in schema * **Unused in schema**: key is declared in schema but not referenced in code Pure execution-environment plumbing, meaning variables that reflect *where/how* the process runs (e.g. `PATH`, `HOME`, `SHELL`, `NODE_OPTIONS`, `npm_*`), is read from `process.env` in normal code but is never part of your schema, so it is **not** reported as missing. Semantically meaningful variables your app or CI may depend on (e.g. `NODE_ENV`, `CI`, GitHub Actions vars) are still reported, so you can decide whether to declare them or suppress them with [`@auditIgnore`](/reference/item-decorators/#auditignore). Exit codes: * `0` when schema and code are in sync * `1` when drift is detected ```bash varlock audit [paths...] [options] ``` **Positional arguments:** * `[paths...]`: Optional list of directories to scan. When provided, only these directories are scanned instead of the auto-detected scan root. **Options:** * [Common options](#common-options): `--path` / `-p` (here it sets the **schema** entry point, single path only) * `--ignore` / `-i`: Directory to exclude from code scanning (can be specified multiple times) **Examples:** ```bash # Audit current project varlock audit # Audit using a specific .env file as schema entry point varlock audit --path .env.prod # Audit using a directory as schema entry point varlock audit --path ./config # Only scan specific directories varlock audit ./src ./lib # Exclude directories from scanning varlock audit --ignore vendor # Exclude multiple directories varlock audit -i vendor -i generated ``` Note When `--path` points to a directory, code scanning is scoped to that directory tree. When it points to a file, scanning is scoped to that file’s parent directory. Monorepos Code scanning does not descend into nested projects. Any subdirectory that contains its own `package.json` or `.env.schema` is treated as a separate package and skipped. This keeps a parent package’s audit from picking up env var references that belong to child packages, and works even in a fresh monorepo where the child packages haven’t run `varlock init` yet. Run `varlock audit` inside each package to audit it against its own schema. Suppressing false positives * Use [`@auditIgnore`](/reference/item-decorators/#auditignore) on individual schema items that are only consumed by external tools and won’t appear in your application code. * Use [`@auditIgnorePaths()`](/reference/root-decorators/#auditignorepaths) to exclude directories (e.g., vendored code, generated files) from the code scan. ### `varlock cache` [Section titled “varlock cache”](#cache) Manage the encrypted **disk cache** used by [`cache()`](/reference/functions/#cache) and plugin authors when cache mode is set to `disk`. When run in a TTY, opens an interactive browser; otherwise prints a status summary. ```bash varlock cache [status|clear] [options] ``` **Sub-commands:** * `status`: print a non-interactive cache status summary (location, file size, entry counts by group) * `clear`: remove cache entries. Requires `--yes` for non-interactive use. **Options:** * `--plugin `: When clearing, only remove entries for a specific plugin * `--yes` / `-y`: Skip confirmation prompts (required when clearing without a TTY) **Examples:** ```bash # Interactive cache browser (or status summary in CI) varlock cache # Print a non-interactive status summary varlock cache status # Clear all cache entries (prompts to confirm) varlock cache clear # Clear all cache entries without confirming (e.g. CI) varlock cache clear --yes # Clear cache for a specific plugin only varlock cache clear --plugin 1password --yes ``` Note When global cache mode is `memory` (or disabled), this command does not show per-process in-memory entries. See the [Caching guide](/guides/caching/) for cache mode strategy and troubleshooting. ### `varlock codegen` [Section titled “varlock codegen”](#codegen) Runs the [code-generation decorators](/reference/root-decorators/#code-generation) in your schema: typed env accessors like `@generateTsTypes` (type declarations) and `@generatePythonEnv` (a loadable module), plus any generators contributed by plugins. Uses only your schema definitions, so output is deterministic regardless of which environment is active. Keys (and decorators) that come only from value files like `.env` or `.env.local` are ignored. Only items declared in your `.env.schema` (or imported into it) are included. When run directly, the command reports any keys found only in a plain `.env` so you can move them into your schema if they belong there. Add a per-language decorator for each output file you want: `@generateTsTypes`, `@generatePythonEnv`, `@generateRustEnv`, `@generateGoEnv`, or `@generatePhpEnv`. Each generates its own file. This command is particularly useful when you have set `auto=false` on a generator decorator to disable automatic generation during `varlock load` or `varlock run`. Note `varlock typegen` is a deprecated alias for `varlock codegen`. It still works but prints a warning. ```bash varlock codegen [options] ``` **Options:** * [Common options](#common-options): `--path` / `-p` **Examples:** ```bash # Generate using the default schema varlock codegen # Generate from a specific .env file varlock codegen --path .env.prod # Generate from multiple directories varlock codegen -p ./envs -p ./overrides ``` ### `varlock telemetry` [Section titled “varlock telemetry”](#telemetry) Opts in/out of anonymous usage analytics. This command creates/updates a configuration file at `$XDG_CONFIG_HOME/varlock/config.json` (defaults to `~/.config/varlock/config.json`) saving your preference. ```bash varlock telemetry disable varlock telemetry enable ``` Note You can also temporarily opt out by setting the `VARLOCK_TELEMETRY_DISABLED` environment variable. See the [Telemetry guide](/guides/telemetry/) for more information about our analytics and privacy practices. ### `varlock generate-key` [Section titled “varlock generate-key”](#generate-key) Generates a random 256-bit encryption key for use with `_VARLOCK_ENV_KEY`. This key is used to encrypt the resolved env blob that gets baked into your build output on certain frameworks/platforms. ```bash varlock generate-key ``` See the [Next.js](/integrations/nextjs/#encrypting-the-env-blob) and [Vite](/integrations/vite/#encrypting-the-env-blob) integration docs for setup instructions. ### `varlock install-plugin` [Section titled “varlock install-plugin”](#install-plugin) Pre-downloads a plugin from npm into the local varlock plugin cache so it is available without an interactive confirmation prompt. This is mainly for the **standalone binary** in CI or other non-interactive environments. When varlock runs as a package.json dependency, plugins resolve through your normal node\_modules instead. The plugin must be specified with an exact version (`name@version`). ```bash varlock install-plugin ``` **Positional arguments:** * ``: The plugin to install, with an exact version (e.g. `my-plugin@1.2.3`). **Examples:** ```bash # Install a plugin at an exact version varlock install-plugin my-plugin@1.2.3 # Scoped package varlock install-plugin @my-scope/my-plugin@2.0.0 ``` ### `varlock help` [Section titled “varlock help”](#help) Displays general help information, alias for `varlock --help` ```bash varlock help ``` For help about specific commands, use: ```bash varlock subcommand --help ``` # @type data types > A reference page of available data types to be used with the `@type` item decorator The [`@type` item decorator](/reference/item-decorators/#type) sets the data type associated with an item. The data type affects coercion, validation, and [generated type files](/reference/root-decorators/#code-generation). ### Additional data type options [Section titled “Additional data type options”](#additional-data-type-options) All types (except `enum`) can be used without any arguments, but most take optional arguments that further narrow the type’s behavior. ```env-spec # @type=string NO_ARGS= # @type=string(minLength=5, maxLength=10, toUpperCase=true) WITH_ARGS= ``` ### Coercion & validation process [Section titled “Coercion & validation process”](#coercion--validation-process) Once a raw value is resolved - which could from a static value in an `.env` file, a [function](/reference/functions/), or an override passed into the process - the raw value will be coerced and validated based on the type, respecting additional arguments provided to the type. Consider the following example: ```env-spec # @type=number(precision=0, max=100) ITEM="123.45" ``` The internal coercion/validation process looks like:\ `"123.45"` -> `123.45` -> `123` -> ❌ invalid (greater than max) ### Default behavior [Section titled “Default behavior”](#default-behavior) When no `@type` is specified, a type will be inferred where possible - for static values, and some functions that return a known type. Note that the use of quotes matters. Otherwise the type will default to `string`. ```env-spec INFERRED_STRING_QUOTED="foo" INFERRED_STRING_UNQUOTED=foo INFERRED_NUMBER=123 # infers number type QUOTED_NUM_STRING="123" # remains a string unless @type=number is used INFERRED_BOOLEAN=true # return type of some functions can be inferred CONCAT_INFERS_STRING=`concat-${SOMEVAR}-will-be-string` FN_INFER_BOOLEAN=eq($VAR1, $VAR2) DEFAULTS_TO_STRING_FN=fnThatCannotInferType() # with no other info, we default to string DEFAULTS_TO_STRING= ``` Note that numeric values that would lose precision, or change any formatting (like leading/trailing zeros), will be treated as strings unless explicitly adding `@type=number`. In any slightly ambiguous situation, it is better to explicitly add a `@type` decorator. ## Built-in data types [Section titled “Built-in data types”](#built-in-data-types) These are the built-in data types. [Plugins](/guides/plugins/) may register additional data types, which appear in [generated code](/guides/code-generation/#extending-with-plugins) according to the coerced type they declare (as strings if they don’t declare one). ### `string` [Section titled “string”](#string) **Options:** * `minLength` (number): Minimum length of the string * `maxLength` (number): Maximum length of the string * `isLength` (number): Exact length required * `startsWith` (string): Required starting substring * `endsWith` (string): Required ending substring * `matches` (string|RegExp): Regular expression pattern to match. Use `/pattern/flags` syntax or a quoted string pattern (see [regex-like strings](/reference/functions#regex-like-strings)) * `toUpperCase` (boolean): Convert to uppercase * `toLowerCase` (boolean): Convert to lowercase * `allowEmpty` (boolean): Allow empty string (default: false) ```env-spec # @type=string(minLength=5, maxLength=10, toUpperCase=true) MY_STRING=value ``` The default type is string No need to add `@type=string` on everything, as it is the default. ### `number` [Section titled “number”](#number) **Options:** * `min` (number): Minimum allowed value (inclusive) * `max` (number): Maximum allowed value (inclusive) * `coerceToMinMaxRange` (boolean): Coerce value to be within `min`/`max` range * `isDivisibleBy` (number): Value must be divisible by this number * `isInt` (boolean): Value must be an integer (equivalent to `precision=0`) * `precision` (number): Number of decimal places to keep ```env-spec # @type=number(min=0, max=100, precision=1) MY_NUMBER=42.5 ``` ### `boolean` [Section titled “boolean”](#boolean) The following values will be coerced to a boolean and considered valid: * True values: `"t"`, `"true"`, `true`, `"yes"`, `"on"`, `"1"`, `1` * False values: `"f"`, `"false"`, `false`, `"no"`, `"off"`, `"0"`, `0` Anything else will be considered invalid. ```env-spec # @type=boolean MY_BOOL=true ``` ### `url` [Section titled “url”](#url) **Options:** * `prependHttps` (boolean): Automatically prepend “https\://” if no protocol is specified * `allowedDomains` (string\[]): List of allowed domains * `noTrailingSlash` (boolean): Disallow a trailing slash on the URL path (except root `/`) * `matches` (string|RegExp): Regular expression pattern the full URL must match. Use `/pattern/flags` syntax or a quoted string pattern (see [regex-like strings](/reference/functions#regex-like-strings)) ```env-spec # @type=url(prependHttps=true) MY_URL=example.com/foobar # @type=url(noTrailingSlash=true, matches=/^https:\/\/api\./) API_URL=https://api.example.com/v1 ``` ### `enum` [Section titled “enum”](#enum) Checks a value is contained in a list of possible values - it must match one exactly. **NOTE** - this is the only type that cannot be used without any additional arguments ```env-spec # @type=enum(development, staging, production) ENV=development ``` ### `email` [Section titled “email”](#email) **Options:** * `normalize` (boolean): Convert email to lowercase ```env-spec # @type=email(normalize=true) MY_EMAIL=User@Example.com ``` ### `port` [Section titled “port”](#port) Checks for valid port number. Coerces to a number. **Options:** * `min` (number): Minimum port number (default: 0) * `max` (number): Maximum port number (default: 65535) ```env-spec # @type=port(min=1024, max=9999) MY_PORT=3000 ``` ### `ip` [Section titled “ip”](#ip) Checks for a valid [IP address](https://en.wikipedia.org/wiki/IP_address). **Options:** * `version` (`4|6`): IPv4 or IPv6 * `normalize` (boolean): Convert to lowercase ```env-spec # @type=ip(version=4, normalize=true) MY_IP=192.168.1.1 ``` ### `semver` [Section titled “semver”](#semver) Checks for a valid [semantic version](https://semver.org/). ```env-spec # @type=semver MY_VERSION=1.2.3-beta.1 ``` ### `isoDate` [Section titled “isoDate”](#isodate) Checks for valid [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date strings with optional time and milliseconds. ```env-spec # @type=isoDate MY_DATE=2024-03-20T15:30:00Z ``` ### `uuid` [Section titled “uuid”](#uuid) Checks for valid [UUID](https://en.wikipedia.org/wiki/UUID) (versions 1-5 per RFC4122, including `NIL`). ```env-spec # @type=uuid MY_UUID=123e4567-e89b-12d3-a456-426614174000 ``` ### `md5` [Section titled “md5”](#md5) Checks for valid [MD5 hash](https://en.wikipedia.org/wiki/MD5). ```env-spec # @type=md5 MY_HASH=d41d8cd98f00b204e9800998ecf8427e ``` ### `simple-object` [Section titled “simple-object”](#simple-object) Validates and coerces JSON strings into objects. ```env-spec # @type=simple-object MY_OBJECT={"key": "value"} ``` ### `duration` [Section titled “duration”](#duration) Flexible duration type. Accepts human-readable strings (`"1h"`, `"30m"`, `"500ms"`, `"2days"`) or bare numbers (interpreted as milliseconds, plain decimals only, no hex/exponent/`Infinity` notation), and outputs a number in the unit you specify. **Options:** * `output`: output unit: `ms` (default), `seconds`, `minutes`, `hours`, `days`, or `weeks` * `min` / `max`: bounds in any duration format (e.g. `min="1s"`, `max="1d"`) ```env-spec # Default: output is milliseconds # @type=duration REQUEST_TIMEOUT=30s # Output in seconds, typical for HTTP client configs # @type=duration(output="seconds") HTTP_TIMEOUT=1h # With min/max bounds # @type=duration(output="minutes", min="1m", max="1d") POLL_INTERVAL=15m ``` Same parser is used by `cache(..., ttl=...)` and the plugin `cacheTtl` option, so any string that works there also works here. For cache mode behavior and troubleshooting, see the [Caching guide](/guides/caching/). # Resolver functions > A reference of all available function resolvers in varlock You may use *resolver functions* instead of static values within both config items and decorator values. Functions can be composed together to create more complex value resolution logic. ```env-spec ITEM=fn(arg1, arg2) COMPOSITION=fn1(fn1Arg1, fn2(fn2Arg1, fn2Arg2)) ``` Note that many built-in utility functions have *expansion* equivalents and often it will be more clear to use them that way. For example: ```env-spec EXPANSION_EQUIVALENT="pre-${OTHER}-post" USING_FN_CALLS=concat("pre-", ref(OTHER), "-post") # mixed example CONFIG=exec(`./scripts/load-config.sh ${APP_ENV}`) ``` There are built-in utility functions, [random value generators](#random-value-generators), a [`cache()`](#cache) function for reusing values according to your global cache mode, encryption functions for device-local secrets, and plugin-provided resolver functions that can fetch data from external providers. See the [Plugins guide](/guides/plugins/) for more information on plugin-provided functions. ## Core [Section titled “Core”](#core) ### `ref()` [Section titled “ref()”](#ref) References another config item (env var) - which is useful when composing multiple functions together. Expansion equivalent: `ref(OTHER_VAR)` === `${OTHER_VAR}` (and also `$OTHER_VAR`) We recommend using the bracketed version within string templates, and the simpler version when referencing an item directly. ```env-spec API_URL=https://api.example.com USERS_API_URL=${API_URL}/users USERS_API_URL2=concat(ref("API_URL"), "/users") # without using expansion ``` ### `concat()` [Section titled “concat()”](#concat) Concatenates multiple values into a single string. Expansion uses `concat()` to combine multiple parts of strings when they include multiple parts. ```env-spec PATH=concat("base/", ref("APP_ENV"), "/config.json") PATH2=`base/${APP_ENV}/config.json` # equivalent using expansion ``` ### `exec()` [Section titled “exec()”](#exec) Executes a CLI command and uses its output as the value. This is particularly useful for integrating with external tools and services. Note Many CLI tools output an additional newline. `exec()` will trim this automatically. Prefer a plugin when one exists If varlock has a [plugin](/plugins/overview/) for your provider (1Password, AWS, Vault, …), use its resolver (e.g. `op()`) instead of shelling out. Plugins handle auth, caching, and validation. Reach for `exec()` only for tools without a plugin, or for custom logic. Expansion equivalent: `exec(command)` === `$(command)` ```env-spec # A custom or internal secrets CLI API_KEY=exec(`my-secrets-cli get api-key`) # Any command whose stdout becomes the value GIT_SHA=exec(`git rev-parse HEAD`) ``` ### `fallback()` [Section titled “fallback()”](#fallback) Returns the first non-empty value in a list of possible values. ```env-spec POSSIBLY_EMPTY= ANOTHER= EXAMPLE=fallback(ref(POSSIBLY_EMPTY), ref(ANOTHER), "default-val") ``` ### `remap()` [Section titled “remap()”](#remap) Maps a value to a new value based on a set of lookup pairs. This is useful for translating one value, often provided by an external platform, into another. * The first argument is the value to remap (often a `ref()` to another variable). * All following arguments are pairs of `(matchValue, resultValue)`. * An optional trailing default value can be added as the last argument (when the total number of remaining args is odd). * Match values can be a string, `undefined`, or a [regex-like string](/reference/functions#regex-like-strings) (`/pattern/`). * If no match is found and there is no default, the original value is returned. ```env-spec # env var that is set by CI/platform CI_BRANCH= # @type=enum(development, preview, production) APP_ENV=remap($CI_BRANCH, "main", production, /.*/, preview, undefined, development) ``` Quoting path-like values An unquoted match value that looks like a valid regex (i.e., `/something/` with optional flags) will be treated as a regex pattern. If you need to match a literal string containing slashes (like a file path), wrap it in quotes. ```env-spec # /usr/local/ looks like a valid regex, so use quotes to match it literally ITEM=remap($PATH_VAR, "/usr/local/", found, default) ``` Deprecated syntax The old key=value syntax (`result=match`) is still supported but deprecated. Use positional pairs instead. ```env-spec # deprecated - key is the result, value is what to match (backwards and limited by key naming) APP_ENV=remap($CI_BRANCH, production="main", preview=/.*/, development=undefined) ``` ## Utilities [Section titled “Utilities”](#utilities) ### `ifs()` [Section titled “ifs()”](#ifs) Evaluates a series of condition/value pairs, returning the value for the first truthy condition. Similar to Excel’s `IFS` function. * Arguments are pairs of `(condition, value)`. * An optional trailing default value can be added as the last argument (when the total number of args is odd). * If no condition is truthy and there is no default, returns `undefined`. ```env-spec ENV=staging # returns the value matching the first truthy condition API_URL=ifs( eq($ENV, production), https://api.example.com, eq($ENV, staging), https://staging-api.example.com, http://localhost:3000 ) ``` ### Regex-like strings [Section titled “Regex-like strings”](#regex-like-strings) Certain functions like `remap()` and type options like `matches` support regex pattern matching. You can use JavaScript-style regex syntax (`/pattern/flags`) as an unquoted value. These will be automatically detected and treated as regular expressions. A string is treated as a regex when it: * Starts and ends with `/` (with optional flags like `i`, `g`, `m`, `s`, `u`, `y` after the closing `/`) * Is **not** wrapped in quotes ```env-spec # regex pattern in remap, matches case-insensitively ENV_TYPE=remap($APP_ENV, /^dev.*/i, dev, "production", prod) # regex pattern in type options # @type=string(matches=/^sk-[a-zA-Z0-9]+$/) API_KEY= # @type=url(matches=/^https:\/\/api\./) API_URL= ``` Paths vs regex ambiguity Since `/something/` looks like a valid regex, values like `/usr/lib/` will be treated as regex patterns in contexts that support them (like `remap()` match values). To use a literal string containing slashes, wrap it in quotes: ```env-spec # unquoted, treated as regex pattern matching "usr/lib" ITEM=remap($VAR, /usr/lib/, matched) # quoted, treated as the literal string "/usr/lib/" ITEM=remap($VAR, "/usr/lib/", matched) ``` Note that as a top-level config value (e.g., `MY_PATH=/usr/local/bin`), slash-containing strings are always treated as plain strings. `regex()` function The older `regex("pattern")` function wrapper is still supported but the `/pattern/` literal syntax is preferred, since it’s more concise and supports flags. ```env-spec # older style - still works ENV_TYPE=remap($APP_ENV, regex("^dev.*"), dev, "production", prod) ``` ### `forEnv()` [Section titled “forEnv()”](#forenv) Resolves to a boolean, if the current [environment](/reference/root-decorators/#currentenv) matches any in the list passed in as args. **Requirements:** * Requires an [`@currentEnv`](/reference/root-decorators/#currentenv) to be set in your `.env.schema` file * Takes one or more environment names as arguments ```env-spec # @currentEnv=$APP_ENV @defaultRequired=false # @disable=forEnv(test) # entire file will be disabled if env is test # --- APP_ENV=staging # Required only in development # @required=forEnv(development) DEV_API_KEY= # Required in staging and production # @required=forEnv(staging, production) PROD_API_KEY= ``` ### `eq()` [Section titled “eq()”](#eq) Checks if 2 values are equal and resolves to a boolean. ```env-spec IS_STAGING_DEPLOYMENT=eq($GIT_BRANCH, "staging") ``` ### `if()` [Section titled “if()”](#if) Checks a boolean to return a true/false option ```env-spec API_URL=if(eq($GIT_BRANCH, "main"), api.example.com, staging-api.example.com) ``` ### `not()` [Section titled “not()”](#not) Negates a value and returns a boolean. Falsy values are - `false`, `""`, `0`, `undefined`, and will be negated to `true`. Otherwise will return `false`. ```env-spec # Negate the result of another function SHOULD_DISABLE_FEATURE=not(forEnv(production)) ``` ### `isEmpty()` [Section titled “isEmpty()”](#isempty) Returns `true` if the value is `undefined` or an empty string, `false` otherwise. ```env-spec # Check if a value is empty HAS_API_KEY=not(isEmpty($API_KEY)) # Use with conditional logic API_URL=if(isEmpty($CUSTOM_API_URL), "https://api.default.com", $CUSTOM_API_URL) ``` ## Random value generators [Section titled “Random value generators”](#random-value-generators) These functions generate random values using cryptographically secure randomness (`node:crypto`). In ephemeral environments, it can be helpful to generate values that are unique per run/deployment. For local dev, the [`cache()`](#cache) function can be used to keep a value stable for a period of time. ### `randomNum()` [Section titled “randomNum()”](#randomnum) Generates a random number. **Integer by default**; if you pass `precision=N`, returns a float with `N` decimal places. * With 1 arg: generates between `0` and `max` (inclusive) * With 2 args: generates between `min` and `max` (inclusive) * `precision=N` option switches to float mode (decimal places, 0-20) ```env-spec # Cached random port between 3000 and 4000 (integer) DEV_PORT=cache(randomNum(3000, 4000)) # One-off random integer up to 1000 SEED=randomNum(1000) # Random float between 0 and 1 with 4 decimal places RATE=randomNum(0, 1, precision=4) # Cached random float between 10 and 20 with 4 decimal places THRESHOLD=cache(randomNum(10, 20, precision=4)) ``` ### `randomUuid()` [Section titled “randomUuid()”](#randomuuid) Generates a random UUID v4. ```env-spec # Unique identifier for this environment (stable across runs) INSTANCE_ID=cache(randomUuid()) # Per-run / per-evaluation ID REQUEST_ID=randomUuid() ``` ### `randomHex()` [Section titled “randomHex()”](#randomhex) Generates a random hexadecimal string. By default the argument is the **character length** of the output. Pass `bytes=true` to interpret the argument as a byte count instead (where each byte = 2 hex characters). Default is `32` characters. ```env-spec # 32-character hex string (default) # @sensitive SESSION_SECRET=cache(randomHex()) # 64-character hex string # @sensitive ENCRYPTION_KEY=cache(randomHex(64)) # 32 bytes (= 64 hex chars), byte-length mode # @sensitive HMAC_KEY=cache(randomHex(32, bytes=true)) # Non-sensitive one-off value NONCE=randomHex(16) ``` ### `randomString()` [Section titled “randomString()”](#randomstring) Generates a random alphanumeric string. Default length is `16` characters using `A-Za-z0-9`. * First arg: character length (default: 16) * `charset=S` option: custom character set to draw from ```env-spec # 32-character alphanumeric string # @sensitive API_SECRET=cache(randomString(32)) # 8-character string from custom charset PIN_CODE=cache(randomString(8, charset="0123456789")) # One-off random string TEMP_LABEL=randomString(10) ``` ## Caching [Section titled “Caching”](#caching) ### `cache()` [Section titled “cache()”](#cache) Wraps any resolver to cache its result. See [caching guide](/guides/caching/) for more details. * First arg: the resolver to cache * `ttl=D` option: how long to cache (default: `forever`). Accepts duration strings with `ms`, `s`, `m`, `h`, `d`, `w` suffixes (long forms and plurals also work), bare numbers as milliseconds, or the keyword `forever` to cache until manually cleared. * `key=S` option: use an explicit cache key instead of the auto-generated one. Useful when the same cached value should be shared across files or when you want a stable key that doesn’t change with resolver edits. * Keys must be non-empty printable text (no control characters) and at most 2048 characters. The cache automatically invalidates when you change the wrapped resolver expression (unless using a custom `key`). ```env-spec # Cache a random UUID forever (until manually cleared) INSTANCE_ID=cache(randomUuid()) # Cache an API token for 1 hour AUTH_TOKEN=cache(exec(`get-token.sh`), ttl="1h") # Cache for 30 minutes TEMP_KEY=cache(randomHex(32), ttl="30m") # Use an explicit cache key (shared across files/projects) SHARED_TOKEN=cache(exec(`fetch-org-token.sh`), ttl="1d", key="org-auth-token") ``` Use the [`varlock cache`](/reference/cli-commands/#cache) CLI command to view or clear the **disk cache**. Use `--clear-cache` or `--skip-cache` flags on `varlock load` / `varlock run` / `varlock printenv` to control caching behavior for a single invocation. Global cache behavior is configured with the [`@cache` root decorator](/reference/root-decorators/#cache). In `memory` mode, `varlock cache` will not show in-memory entries from a running process. For strategy recommendations and troubleshooting, see the [Caching guide](/guides/caching/). Tip Plugin authors can also use the cache API via `plugin.cache.getOrSet()` (or `get()` / `set()`) to cache expensive API calls. See the [Plugins guide](/guides/plugins/#plugin-caching) for more information. ## Encryption [Section titled “Encryption”](#encryption) ### `varlock()` [Section titled “varlock()”](#varlock) Decrypts a locally encrypted value, or prompts for a new secret to encrypt. This is the built-in resolver for varlock’s [device-local encryption](/guides/local-encryption/) feature. **Decrypt mode**: pass an encrypted payload to decrypt at load time: ```env-spec # @sensitive API_KEY=varlock("local:") ``` **Prompt mode**: prompts the user to enter a secret, encrypts it, and writes the encrypted value back to the source file: ```env-spec # @sensitive API_KEY=varlock(prompt) # also valid as a key=value param: API_KEY=varlock(prompt=1) ``` On first run with `prompt` mode, you’ll be asked to enter the secret value. Once entered, the file is automatically updated with the encrypted payload. On macOS with Secure Enclave, a native dialog with biometric authentication is used. Values are encrypted using the best available backend on your platform. See the [Local encryption guide](/guides/local-encryption/) for details. Encrypted payload lifecycle: * Store encrypted payloads (`varlock("local:...")`) in local override files (typically `.env.local`) * Decryption happens at runtime during `varlock load` / `varlock run` * Use [`varlock reveal`](/reference/cli-commands/#reveal) when you need to inspect a decrypted value interactively ### `keychain()` [Section titled “keychain()”](#keychain) Reads a secret from the **macOS Keychain**. This built-in resolver communicates through Varlock’s native Swift daemon, enforcing biometric (Touch ID) authentication and per-session access control. See the [macOS Keychain page](/plugins/macos-keychain/) for full documentation. **Array args:** * `service` (optional): Service name of the keychain item (positional shorthand) * `prompt` (optional): Enter interactive picker mode **Key/value args:** * `service` (optional): Service name of the keychain item * `account` (optional): Account identifier for the keychain item * `keychain` (optional): Name of a specific keychain to search (e.g., `"System"`) * `field` (optional): Specific field to extract from the keychain item * `prompt` (optional): If set, opens a native picker dialog for interactive selection ```env-spec # Positional shorthand DATABASE_PASSWORD=keychain("com.company.database") # Named service with account ADMIN_PW=keychain("com.company.db", account="admin") # Interactive picker mode, writes back resolved reference NEW_SECRET=keychain(prompt) ``` Caution `keychain()` is only available on macOS. For cross-platform local encryption, use [`varlock()`](#varlock) instead. # Config Item @decorators > A reference page of available env-spec decorators for items Decorators in a comment block *directly* preceding a config item will be attached to that item. Multiple decorators can be specified on the same line. A comment block is broken by either an empty line or a divider (`# ---` is optional, but often used for clarity). ```env-spec # @required @sensitive @type=string(startsWith=sk-) # @docs(https://docs.servicex.com/api-keys) SERVICE_X_API_KEY= ``` More details of the minutiae of decorator handling can be found in the [@env-spec reference](/env-spec/reference/#comments-and-decorators). ## Built-in item decorators [Section titled “Built-in item decorators”](#built-in-item-decorators) These are the item decorators built into Varlock. [Plugins](/guides/plugins/) can extend the DSL with additional decorators. ### `@required` [Section titled “@required”](#required) **Value type:** `boolean` Sets whether an item is *required* - meaning validation will fail if the value resolves to `undefined` or an empty string. Default behavior for all items within the same file can be toggled using the [`@defaultRequired` root decorator](/reference/root-decorators/#defaultrequired). 💡 Use the [`forEnv()` function](/reference/functions/#forenv) to set required based on the current environment. ```env-spec # @defaultRequired=false # --- # @required # same as @required=true REQUIRED_ITEM= # @required=forEnv(prod) REQUIRED_FOR_PROD_ITEM= # @required=eq($OTHER, foo) REQUIRED_IF_OTHER_IS_FOO= ``` ### `@optional` [Section titled “@optional”](#optional) **Value type:** `boolean` Opposite of [`@required`](#required). Equivalent to writing `@required=false`. ```env-spec # @defaultRequired=true # --- # @optional OPTIONAL_ITEM= ``` ### `@sensitive` [Section titled “@sensitive”](#sensitive) **Value type:** `boolean` Sets whether the item should be considered *sensitive* - meaning it cannot be exposed to the public. The value will be always be redacted in CLI output, and client integrations can take further action to prevent leaks. Default behavior for all items can be set using the [`@defaultSensitive` root decorator](/reference/root-decorators/#defaultsensitive) Sensitivity may also be implied by the item’s [data type](/reference/data-types). For example, plugin “secret-zero” credential types (like a [1Password `opServiceAccountToken`](/plugins/1password/)) are sensitive by default, so you don’t need to add `@sensitive` explicitly (use `@public` or `@sensitive=false` to override). An explicit `@sensitive` on the item always wins. 📖 See the [secrets management guide](/guides/secrets/) for best practices on handling sensitive values, and how to use plugins to fetch them from secret management platforms. ```env-spec # @sensitive SERVICE_X_PRIVATE_KEY= # @sensitive=false SERVICE_X_CLIENT_ID= ``` **Per-item options:** pass an object value to tune protection for a single sensitive item: * `enabled` (`boolean`, default `true`): whether the item is sensitive. Lets you keep options while toggling sensitivity, including dynamically via a function (e.g. `enabled=forEnv(production)`). `@sensitive={enabled=false}` is equivalent to `@sensitive=false`. * `preventLeaks` (`boolean`, default `true`): when `false`, this item’s value is excluded from [leak detection](/reference/root-decorators/#preventleaks), so it will *not* trigger an error if it appears in a response. Useful when an endpoint legitimately returns a secret to another system. The value is still redacted in logs. ```env-spec # @sensitive={preventLeaks=false} TOKEN_FORWARDED_TO_PARTNER= # sensitive only in production, and allowed to leave the system # @sensitive={enabled=forEnv(production), preventLeaks=false} PARTNER_TOKEN= ``` Use `preventLeaks=false` sparingly Leak detection is a safety net that stops a secret from accidentally being sent in a response. Setting `preventLeaks=false` removes that net for this item, so only use it when a secret is *deliberately* meant to leave the system (e.g. an endpoint forwarding a token to another service), never just to silence a leak-detection error. Prefer disabling it for a single item like this over turning off [`@preventLeaks`](/reference/root-decorators/#preventleaks) globally. ### `@public` [Section titled “@public”](#public) **Value type:** `boolean` Opposite of [`@sensitive`](#sensitive). Equivalent to writing `@sensitive=false`. ```env-spec # @defaultSensitive=true # --- # @public PUBLIC_API_URL=https://api.example.com ``` ### `@internal` [Section titled “@internal”](#internal) **Value type:** `boolean` Marks an item as used **only internally by varlock**, not by your application. Internal items are still resolved and can be referenced by other items (e.g. via [`ref()`](/reference/functions/#ref) or string expansion), but they are **excluded** from everything that reaches your app: the injected env vars, the [serialized graph](/reference/reserved-variables/#__varlock_env) blob, and generated types. The most common use is a “secret zero”: a credential used only to fetch *other* secrets (for example a vault/secrets-manager token). Your application never needs it, and keeping it out of the process environment reduces your attack surface. ```env-spec # the token below is only used to resolve the secrets that follow it # @internal @sensitive OP_TOKEN= # resolved using OP_TOKEN, but only DATABASE_URL is injected into the app # @sensitive DATABASE_URL=exec(`op read "op://app/db/url"`) ``` Some plugin data types are **internal by default**. For example, a [1Password `opServiceAccountToken`](/plugins/1password/) or other secrets-manager credential authenticates varlock but is rarely needed in app code. You can opt back in with `@internal=false`: ```env-spec # @type=opServiceAccountToken @internal=false OP_TOKEN= ``` Marking something internal stops injecting it Because internal items still resolve and validate, marking a variable internal does **not** produce an error if your application actually reads it. The value simply won’t be in the environment at runtime. If your app uses one of these credentials directly (e.g. to write secrets back, renew Vault leases, or fetch more secrets at runtime), set `@internal=false` so it keeps being injected. Note `@internal` controls what varlock *injects*, not what it shows. Internal items still appear (redacted) in inspection output (the `varlock load` summary and `varlock load --agent`), so they can be set and debugged. `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. ### `@type` [Section titled “@type”](#type) **Value type:** [`data type`](/reference/data-types) (name only or function call) Sets the data type of the item - which affects validation, coercion, and generated types. Note that some data types take additional arguments. See [data types reference](/reference/data-types) for more details. If not specified, a data type will be inferred when possible, or default to `string` otherwise. ```env-spec # @type=url # name only SOME_URL= # @type=string(startsWith=abc) # function call with options EXAMPLE_WITH_TYPE_OPTIONS= INFER_NUMBER=123 # data type of `number` will be inferred from the value ``` ### `@example` [Section titled “@example”](#example) **Value type:** `string` Provides an example value for the item. This lets you avoid setting placeholder values that are not meant to be used. ```env-spec # @example="sk-abc123" SECRET_KEY= ``` ### `@docs()` [Section titled “@docs()”](#docs) **Arg types:** `[ url: string ] | [ description: string, url: string ]` URL of documentation related to the item. Will be included in [generated types](/reference/root-decorators/#code-generation). *Can be called multiple times.* ```env-spec # @docs(https://xyz.com/docs/api-keys) # @docs("Authentication guide", https://xyz.com/docs/auth-guide) XYZ_API_KEY= ``` ![example of docs() in generated types](/_astro/multiple-docs-intellisense.DsdGRzO3.png)*example of `docs()` info in generated types / IntelliSense* ### `@docsUrl` (deprecated) [Section titled “@docsUrl (deprecated)”](#docsurl) **Value type:** `string` URL of documentation related to the item. Use [`@docs()`](#docs) instead, which supports multiple docs entries with optional descriptions. `@docsUrl=https://example.com` -> `@docs(https://example.com)` ### `@icon` [Section titled “@icon”](#icon) **Value type:** `string` Attaches an icon identifier to the item, using iconify ids `collection-name:icon-name`. This icon will be included in autogenerated types. See to browse available icons. Useful for generated docs and UI surfaces that show schema metadata. ```env-spec # @icon=mdi:key SERVICE_X_API_KEY= ``` ### `@auditIgnore` [Section titled “@auditIgnore”](#auditignore) **Value type:** `boolean` Suppresses “unused in schema” warnings from [`varlock audit`](/reference/cli-commands/#audit) for this item. Useful for items that are only consumed by external tools and won’t appear in your application code. ```env-spec # @auditIgnore # used by CI tooling, not referenced in code CI_DEPLOY_TOKEN= ``` # Reserved variables > varlock's reserved _VARLOCK_* and __VARLOCK_* environment variables: what they do and how the reserved namespace behaves varlock reserves a couple of environment variable namespaces for its own use. Knowing them helps you avoid surprises and understand how varlock passes state between processes. | Prefix | Example | Who sets it | | --------------------------------- | ------------------ | -------------------------------------------------------------------------- | | `VARLOCK_*` (no underscore) | `VARLOCK_ENV` | varlock (computed); see [Builtin variables](/reference/builtin-variables/) | | `_VARLOCK_*` (single underscore) | `_VARLOCK_ENV_KEY` | **you**, to configure varlock’s behavior | | `__VARLOCK_*` (double underscore) | `__VARLOCK_ENV` | varlock internals, injected automatically; never set these yourself | The `_VARLOCK_` prefix is reserved Config items whose key starts with `_VARLOCK_` are reserved for varlock. They are **excluded** from the injected env blob, generated types, and override provenance, even if you define one in your `.env.schema`. varlock emits a warning if you do, since such an item won’t behave like a normal config value. Don’t name your own variables with this prefix. ## Configuration variables (`_VARLOCK_*`) [Section titled “Configuration variables (\_VARLOCK\_\*)”](#configuration-variables-_varlock_) Set these in your shell, CI, or deploy environment to configure varlock’s behavior. ### `_VARLOCK_ENV_KEY` [Section titled “\_VARLOCK\_ENV\_KEY”](#_varlock_env_key) Encryption key used to decrypt the injected env blob and any `encrypted()` values at runtime. Typically set in deploy environments. See [Encrypted deployments](/guides/encrypted-deployments/). ### `_VARLOCK_CACHE_KEY` [Section titled “\_VARLOCK\_CACHE\_KEY”](#_varlock_cache_key) Encryption key for the on-disk resolved-value cache. When set (e.g. as a CI secret), it enables the disk cache in environments without OS keychain access. See [Caching](/guides/caching/). ### `_VARLOCK_REDACT_STDOUT` [Section titled “\_VARLOCK\_REDACT\_STDOUT”](#_varlock_redact_stdout) Overrides [`varlock run`](/reference/cli-commands/#run) output redaction: * `true` / `1`: force redaction on (only applies to piped/redirected output; errors if attached to an interactive terminal) * `false` / `0`: disable redaction entirely The `--redact-stdout` / `--no-redact-stdout` flags take precedence over this env var. Useful when you can’t easily change the command being run, for example in a wrapper script or CI config. ## Internal variables (`__VARLOCK_*`) [Section titled “Internal variables (\_\_VARLOCK\_\*)”](#internal-variables-__varlock_) These are injected automatically so that varlock state survives across process boundaries. You should never set them yourself. They’re documented here only so you recognize them if you see them in a process environment. ### `__VARLOCK_ENV` [Section titled “\_\_VARLOCK\_ENV”](#__varlock_env) The serialized env graph (resolved config values plus metadata) injected by [`varlock run`](/reference/cli-commands/#run) and build-time integrations, so the runtime can load your config without re-invoking the CLI. ### `__VARLOCK_RUN` [Section titled “\_\_VARLOCK\_RUN”](#__varlock_run) A marker set so a child process can detect that it is running under `varlock run`. # Root @decorators > A reference page of available env-spec decorators that apply to the schema itself, rather than individual items Root decorators appear in the *header* section of a .env file - which is any comment block(s) at the beginning of the file, before the first config item. Usually root decorators are used only in your `.env.schema` file. .env.schema ```env-spec # This is the header, it can contain root decorators # @defaultSensitive=false @defaultRequired=infer # @generateTsTypes(path=./env.d.ts) ``` More details of the minutiae of decorator handling can be found in the [@env-spec reference](/env-spec/reference/#comments-and-decorators). ## General root decorators [Section titled “General root decorators”](#general-root-decorators) These are the root decorators that are built into Varlock. [Plugins](/guides/plugins/) may introduce more. ### `@currentEnv` [Section titled “@currentEnv”](#currentenv) **Value type:** [`ref()`](/reference/functions/#ref) (usually written as `$ITEM_NAME`) Sets the current *environment* value, which will be used when determining if environment-specific .env files will be loaded (e.g. `.env.production`), and also may affect other dynamic behaviour in your schema, such as the [`forEnv()` function](/reference/functions/#forenv). We refer to the name of this item as your *environment flag*. * It *must* be set to a simple reference to a single config item (e.g. `$APP_ENV`). * This decorator should only be set in your `.env.schema` file. * The referenced item *must* be defined within the same file. * This will override the `--env` CLI flag if it is set. * We do not recommend using `NODE_ENV` as your environment flag, as it has other implications, and is often set out of your control. See [environments guide](/guides/environments) for more info. ```env-spec # @currentEnv=$APP_ENV # --- # @type=enum(dev, preview, prod, test) APP_ENV=dev ``` ### `@envFlag` (deprecated) [Section titled “@envFlag (deprecated)”](#envflag) **Value type:** `string` (must be a valid item name within same file) Sets the current *environment flag* by name. ⚠️ Deprecated at v0.1 - use [`@currentEnv`](#currentenv) instead. `@envFlag=APP_ENV` -> `@currentEnv=$APP_ENV` ### `@defaultRequired` [Section titled “@defaultRequired”](#defaultrequired) **Value type:** `boolean | "infer"` Sets the default behavior of each item being *required*. Only applied to items that have a definition within the same file. Can be overridden on individual items using [`@required`](/reference/item-decorators/#required)/[`@optional`](/reference/item-decorators/#optional). * `infer` (default): Items with a value set in the same file will be required; items with an empty string or no value are optional. * `true`: All items are required unless marked optional. * `false`: All items are optional unless marked required. ```env-spec # @defaultRequired=infer # --- FOO=bar # required (static value) BAR=fnCall() # required (function value) BAZ= # optional (no value) QUX='' # optional (empty string) # @optional OPTIONAL_ITEM=foo # optional (explicit) # @required REQUIRED_ITEM= # required (explicit) ``` ### `@defaultSensitive` [Section titled “@defaultSensitive”](#defaultsensitive) **Value type:** `boolean | inferFromPrefix(PREFIX)` Sets the default state of each item being treated as [*sensitive*](/guides/secrets/). Only applied to items that have a definition within the same file. Can be overridden on individual items using [`@sensitive`](/reference/item-decorators/#sensitive). * `true` (default): All items are sensitive unless marked otherwise. * `false`: All items are not sensitive unless marked otherwise. * `inferFromPrefix(PREFIX)`: Item is marked not sensitive if key starts with the given `PREFIX`; all others are sensitive. Useful for marking e.g. `PUBLIC_` keys as non-sensitive by default. 📖 See the [secrets management guide](/guides/secrets/) for best practices on handling sensitive values, and how to use plugins to fetch them from secret management platforms. ```env-spec # @defaultSensitive=inferFromPrefix(PUBLIC_) # --- PUBLIC_FOO= # not sensitive (due to matching prefix) OTHER_FOO= # sensitive (default when prefix does not match) # @sensitive PUBLIC_BAR= # sensitive (explicit decorator overrides prefix) # @sensitive=false OTHER_BAR= # not sensitive (explicit) ``` ### `@disable` [Section titled “@disable”](#disable) **Value type:** `boolean` If true, disables loading the file - meaning no items or plugins are loaded from it. Useful for temporarily or conditionally disabling a `.env` file. 💡 The [`forEnv()`](/reference/functions/#forenv) function can disable an explicitly [imported](/guides/import/) file based on the current [environment](/guides/environments/). ```env-spec # @disable # (shorthand for @disable=true) # # @plugin(@varlock/x-plugin) # will not be loaded # --- FOO=bar # will be ignored ``` ### `@import()` [Section titled “@import()”](#import) **Arg types:** `[ path: string ]`\ **Named args:** `enabled?: boolean`, `allowMissing?: boolean`, `pick?: string[]`, `omit?: string[]` Imports other `.env` file(s) - useful for sharing config across monorepos and splitting up large schemas. *Can be called multiple times.* You may import a specific file, or a directory of files. Importing a **directory** (trailing `/`) brings everything that directory would load on its own (its `.env.schema` plus sibling `.env`, `.env.local`, and environment-specific files, resolved by the current environment flag), whereas importing a single file brings only that file. To pull in a sibling or root package’s values (not just its schema), import the directory. The optional `enabled` parameter allows conditional imports based on boolean expressions. It defaults to `true` if not specified. The optional `allowMissing` parameter makes the import optional - if set to `true`, the import will be silently skipped if the file or directory doesn’t exist instead of causing a loading error. It defaults to `false` if not specified. **Filtering keys:** by default every key from the imported file(s) is brought in. Use `pick` to import only an allowlist of keys, or `omit` to import everything except a denylist. You can’t use both in one import, and both accept simple globs (`*`, `?`). Across nested imports, filters intersect: a key must pass every filter in the chain. Positional keys are deprecated Listing keys as positional args (`@import(./.env.other, KEY1, KEY2)`) is **deprecated**; use `pick=[KEY1, KEY2]` instead. The positional form still works (exact-match only, no globs) but will warn. See the [imports guide](/guides/import/) for more details and advanced usage. ```env-spec # @import(./.env.imported) # import a specific file # @import(./.env.other, pick=[KEY1, KEY2]) # import only these keys (allowlist) # @import(./.env.other, pick=[API_*]) # globs are supported # @import(./.env.other, omit=[SECRET]) # import everything except these (denylist) # @import(../shared-env/) # import a directory # @import(~/.env.shared) # import from home directory # @import(./.env.dev, enabled=eq($ENV, "dev")) # conditional import # @import(./.env.local, allowMissing=true) # optional import (no error if missing) # --- # this definition is merged with any found in imports, but this one has more precedence IMPORTED_ITEM=overridden-value ``` ### `@setValuesBulk()` [Section titled “@setValuesBulk()”](#setvaluesbulk) **Arg types:** `[ data: string ]` **Named args:** `format?: "json" | "env"`, `createMissing?: boolean`, `enabled?: boolean`, `pick?: string[]`, `omit?: string[]` Injects multiple config values at once from an external data source. The first argument is a resolver that produces a string, typically a bulk resolver from a [secrets provider plugin](/plugins/overview/) such as [`opLoadEnvironment()`](/plugins/1password/#oploadenvironment) (1Password), [`infisicalBulk()`](/plugins/infisical/), or [`vaultSecret(…, raw=true)`](/plugins/hashicorp-vault/) (HashiCorp Vault). The string is parsed and injected as definitions within the file containing the decorator. Bulk values participate in the normal file override chain: `process.env` still overrides everything, higher-precedence files (`.env.local`, `.env.production`, etc.) override bulk values, and bulk values override schema-defined defaults in the same file. You control precedence by choosing which file to put `@setValuesBulk` in. **Options:** * `format`: How to parse the data string. `json` expects a flat JSON object, `env` expects `.env` file format. If not specified, auto-detected by checking if the string starts with `{`. * `createMissing`: If `true`, keys in the bulk data that don’t already exist in your schema will be created as new config items. Defaults to `false` (unknown keys are silently skipped). * `enabled`: If `false`, the bulk data resolver is skipped entirely. Accepts any boolean expression, including dynamic references to other config items. Defaults to `true`. * `pick`: An array of key names to inject, an **allowlist**. Only matching keys are injected; everything else from the source is ignored. * `omit`: An array of key names to skip, a **denylist**. Every key *except* the matches is injected. By default (neither `pick` nor `omit`) every key from the source is injected. You can’t use `pick` and `omit` together. Both accept simple glob patterns: `*` matches any run of characters and `?` matches a single character (e.g. `pick=[API_*]`). *Can be called multiple times; later calls overwrite earlier ones for the same keys.* .env.schema ```env-spec # Load all values from a 1Password environment # @plugin(@varlock/1password-plugin) # @initOp(token=$OP_TOKEN, allowAppAuth=forEnv(dev)) # @setValuesBulk(opLoadEnvironment(your-environment-id)) # --- # @type=opServiceAccountToken @sensitive OP_TOKEN= API_KEY= DB_PASSWORD= ``` .env.schema ```env-spec # Inject from a .env-style blob stored in a single 1Password field # @setValuesBulk(op("op://vault/app-secrets/dotenv"), format=env) # --- API_KEY= DB_PASSWORD= ``` .env.schema ```env-spec # Allowlist: only inject these keys (globs allowed, e.g. API_*) # @setValuesBulk(opLoadEnvironment(your-environment-id), pick=[API_KEY, DB_PASSWORD]) # --- API_KEY= DB_PASSWORD= ``` .env.schema ```env-spec # Denylist: inject everything except these keys # @setValuesBulk(infisicalBulk(), omit=[LEGACY_TOKEN, DEBUG_*]) ``` .env.schema ```env-spec # Create items not already in the schema # @setValuesBulk(infisicalBulk(), createMissing=true) ``` .env.schema ```env-spec # Only fetch from the source in non-production environments # @setValuesBulk(opLoadEnvironment(dev-environment-id), enabled=eq($APP_ENV, "dev")) # --- API_KEY= APP_ENV=dev ``` If a plugin doesn’t exist for your provider, any CLI tool works via [`exec()`](/reference/functions/#exec), e.g. `@setValuesBulk(exec("my-secrets-cli export --format=json"), format=json)`. Note When using `format=env`, function calls like `$VAR` references in unquoted values are not supported and will cause an error. Use single quotes for literal `$` signs (e.g., `'$LITERAL'`) or use `format=json` instead. `createMissing` and type generation Items created via `createMissing=true` (those that don’t already exist in your schema) **will not be included in generated TypeScript types**. Type generation happens at build/schema-load time before values are fetched, so dynamically created keys are invisible to the type generator. For this reason, **`createMissing=true` is not recommended**. Instead, declare all expected items explicitly in your schema (with an empty value) so they are known at type-generation time. ### `@plugin()` [Section titled “@plugin()”](#plugin) **Arg types:** `[ identifier: string ]` Loads a plugin, which can register new root decorators, item decorators, and resolver functions. *Can be called multiple times.* See [plugins guide](/guides/plugins/) for more details. ```env-spec # @plugin(@varlock/1password-plugin) # @initOp(allowAppAuth=true) # new root decorator # --- # @type=opServiceAccountToken # new data type OP_TOKEN= # @sensitive XYZ_API_KEY=op(op://api-prod/xyz/api-key) # new resolver ``` ### `@cache` [Section titled “@cache”](#cache) **Value type:** `"auto" | "memory" | "disk" | "disabled"` Sets global cache mode for [`cache()`](/reference/functions/#cache) and plugin cache APIs. **Modes:** * `memory`: cache only in-process (not persisted) * `disk`: cache to encrypted disk storage (persists across invocations) * `disabled`: disable caching globally * `auto` (default): uses `disk` when a native encryption backend is available outside CI; otherwise uses `disk` encrypted with `_VARLOCK_CACHE_KEY` if that env var is set, falling back to `memory` (see the [Caching guide](/guides/caching/)) The value can also be set dynamically with a function, for example to change cache mode per environment. A function that resolves to no value falls back to `auto`. ```env-spec # @cache=memory # --- SESSION_SECRET=cache(randomHex(32)) ``` ```env-spec # dynamic - only use disk caching in development # @cache=forEnv(dev, "disk") # --- ``` Note `--skip-cache` on `varlock load` / `varlock run` / `varlock printenv` disables cache reads and writes for that invocation, regardless of `@cache`. See the [Caching guide](/guides/caching/) for mode selection guidance by environment. ### `@redactLogs` [Section titled “@redactLogs”](#redactlogs) **Value type:** `boolean` Controls whether sensitive config values are automatically redacted from console output. When enabled, any sensitive values will be replaced with `▒▒▒▒▒` in logs. *Only applies in JavaScript based projects where varlock runtime code is imported.* * `true` (default): Console logs are automatically redacted * `false`: Console logs are not redacted (useful for debugging) ```env-spec # @redactLogs=false # --- SECRET_KEY=my-secret-value # @sensitive ``` ```js console.log(process.env.SECRET_KEY) // This will log "my▒▒▒▒▒" instead of "my-secret-value" when @redactLogs=true ``` Caution There is a potential performance impact for both `@preventLeaks` and `@redactLogs` when enabled. It depends on the integration and how your application is served. Please [reach out](https://chat.dmno.dev) if you have any questions. We feel that they are beneficial enough to have them on by default but you can always opt out if you prefer. ### `@preventLeaks` [Section titled “@preventLeaks”](#preventleaks) **Value type:** `boolean` Controls whether leak prevention is enabled. When enabled, varlock will scan outgoing HTTP responses to detect if sensitive values are being leaked. *Only applies in JavaScript based projects where varlock runtime code is imported.* **Options:** * `true` (default): Leak detection is enabled * `false`: Leak detection is disabled (useful for debugging) ```env-spec # @preventLeaks=false # --- SECRET_KEY=my-secret-value # @sensitive ``` To opt out leak detection for a *single* item (for example a secret an endpoint legitimately returns to another system) while keeping it enabled everywhere else, use the [`@sensitive` options form](/reference/item-decorators/#sensitive): `@sensitive={preventLeaks=false}`. The value is still redacted in logs. ![Leak prevention](/_astro/leak.D-sjPUs5_1XAfab.png) *a sample leak detection warning in an [Astro project](/integrations/astro/)* Caution See note on [`@redactLogs`](#redactlogs) about potential performance impact. ### `@encryptInjectedEnv` [Section titled “@encryptInjectedEnv”](#encryptinjectedenv) **Value type:** `boolean` Controls whether the injected env blob in server-side build output is encrypted. When enabled, varlock encrypts the blob with AES-256-GCM using the `_VARLOCK_ENV_KEY` environment variable. This is primarily relevant for serverless deployments (Vercel, Netlify, etc.) where varlock injects resolved env data into the build output. The blob only appears in server-side code and is generally safe, but encryption provides extra protection, particularly against secrets leaking via sourcemaps. **Options:** * `false` (default): Blob is injected as plaintext JSON * `true`: Blob is encrypted, `_VARLOCK_ENV_KEY` required at build time and runtime * `forEnv(...)`: Conditionally enable based on the current environment ```env-spec # @encryptInjectedEnv # --- SECRET_KEY= # @sensitive ``` ```env-spec # only encrypt in production # @encryptInjectedEnv=forEnv(prod) # --- SECRET_KEY= # @sensitive ``` **Key management:** * For **Cloudflare Workers**, the key is auto-generated and uploaded as a secret binding, so no manual setup is needed * For **local dev**, a temporary key is auto-generated when running dev servers * For **production deploys** to other platforms, you must set `_VARLOCK_ENV_KEY` on your platform (see the [encrypted deployments guide](/guides/encrypted-deployments/)) ### `@disableProcessEnvInjection` [Section titled “@disableProcessEnvInjection”](#disableprocessenvinjection) **Value type:** `boolean` Prevents varlock from injecting resolved values into `process.env`. When enabled, env vars are only accessible via the `ENV` proxy; `process.env.MY_VAR` will not contain varlock-resolved values. This is useful for security hardening. Combined with [`@encryptInjectedEnv`](#encryptinjectedenv), it ensures that no plaintext secrets exist in `process.env` at all. When set, [`@generateTsTypes`](#generatetstypes) stops augmenting `process.env` by default (its `processEnv` option defaults to `none`), so your generated types match the fact that `process.env` isn’t populated. **Options:** * `false` (default): Values are injected into `process.env` as usual * `true`: Values are only accessible via `ENV` The value must be static (`true`/`false`). Because code generation reads this flag, an environment-dependent value would make generated output differ per environment. ```env-spec # @disableProcessEnvInjection # --- SECRET_KEY= # @sensitive PUBLIC_URL= ``` Caution When enabled, any code that reads from `process.env` directly (including third-party libraries) will not see varlock-resolved values. Make sure all env access in your application goes through the `ENV` proxy. Note This decorator only affects the runtime `initVarlockEnv()` behavior. It does **not** affect [`varlock run`](/reference/cli-commands/#run), which always injects individual env vars into the child process by default. Use `varlock run --no-inject-vars` if you also want to skip individual var injection when using `varlock run`. ### `@auditIgnorePaths()` [Section titled “@auditIgnorePaths()”](#auditignorepaths) **Arg types:** `[ ...paths: string[] ]` Excludes directories from the [`varlock audit`](/reference/cli-commands/#audit) code scanner. Paths are relative to the file containing the decorator. *Can be called multiple times; paths are merged additively.* Useful for excluding generated code, vendored dependencies, or directories that contain env var references you don’t want to audit. ```env-spec # @auditIgnorePaths(vendor, generated/config) # --- API_KEY= ``` ```env-spec # called multiple times, paths are merged # @auditIgnorePaths(vendor) # @auditIgnorePaths(generated/config, scripts/setup) # --- API_KEY= ``` ## Code generation [Section titled “Code generation”](#code-generation) Code-generation decorators turn your schema into generated code, such as TypeScript type declarations or ready-to-use env modules for other languages, and [plugins](/guides/plugins/) can register generators that emit other kinds of code. See the [Code generation guide](/guides/code-generation/) for the full picture, and [Other languages](/integrations/other-languages/) for per-language usage. Every `@generate*` decorator below is a root decorator that *can be called multiple times* (one per output file) and shares these common args: * `path`: Relative filepath to write the generated file to. * `auto`: Controls whether generation runs automatically on every load (defaults to `true`). Set to `false` to generate only when you run [`varlock codegen`](/reference/cli-commands/#codegen) explicitly, useful in a CI pipeline or a dedicated build step. * `executeWhenImported`: overrides the default of not executing when the containing file is imported (defaults to `false`). Tip Usually these decorators live in your primary `.env.schema` file, and are ignored within imported files. To override this, set `executeWhenImported` to `true`. ### `@generateTsTypes()` [Section titled “@generateTsTypes()”](#generatetstypes) TypeScript type declarations. Makes `import { ENV } from 'varlock/env'` typed and augments `process.env` / `import.meta.env`. In addition to the [common code-generation args](#code-generation), it accepts: * `exposeEnv`: where the coerced `ENV` object lives (defaults to `global`). * `global`: globally augment `varlock/env` so `import { ENV } from 'varlock/env'` is typed. * `local`: export a package-local typed `ENV` from the generated file (with **no** global augmentation, since `processEnv`/`importMetaEnv` default to `none` too, so nothing merges across packages). Use this in monorepos where multiple packages have different schemas. Requires a `.ts` output path (not `.d.ts`, since it contains a runtime re-export), and you import from the generated file (e.g. `import { ENV } from './env'`). * `none`: emit only the type definitions, no `ENV` binding. * `processEnv`: how `process.env` is typed: `strict`, `loose`, or `none` (defaults to `strict`; to `none` when `exposeEnv=local`, or when [`@disableProcessEnvInjection`](#disableprocessenvinjection) is set, since values aren’t put on `process.env` then). Note that `@types/node` declares a base string index signature that can’t be removed, so extra keys remain allowed on `process.env` regardless. * `importMetaEnv`: how `import.meta.env` is typed: `strict`, `loose`, or `none` (defaults to `strict`; to `none` when `exposeEnv=local`). ```env-spec # @generateTsTypes(path=./env.d.ts) # monorepo-friendly: importable local ENV, no globals # @generateTsTypes(path=./env.ts, exposeEnv=local) ``` ### `@generatePythonEnv()` [Section titled “@generatePythonEnv()”](#generatepythonenv) A self-contained Python module: a coerced `Env` `TypedDict`, a `load_env()` that parses the injected env, and a `SENSITIVE_KEYS` constant. See [Python usage](/integrations/python/). ```env-spec # @generatePythonEnv(path=./env.py) ``` ### `@generateRustEnv()` [Section titled “@generateRustEnv()”](#generaterustenv) A self-contained Rust module: a serde-derived `Env` struct, a `load()` function, and a `SENSITIVE_KEYS` constant. Requires the `serde` and `serde_json` crates. See [Rust usage](/integrations/rust/). ```env-spec # @generateRustEnv(path=./src/env.rs) ``` ### `@generateGoEnv()` [Section titled “@generateGoEnv()”](#generategoenv) A self-contained Go package: an `Env` struct, a `Load()` function, and a `SensitiveKeys` map. See [Go usage](/integrations/go/). ```env-spec # @generateGoEnv(path=./env/env.go) ``` The package name defaults to the output directory (e.g. `path=env/env.go` → `package env`); override it with `package=`. ### `@generatePhpEnv()` [Section titled “@generatePhpEnv()”](#generatephpenv) A self-contained PHP class: a readonly `Env` with a static `load()` and a `SENSITIVE_KEYS` constant. Requires PHP 8.1+. See [PHP usage](/integrations/php/). ```env-spec # @generatePhpEnv(path=./Env.php) ``` Defaults to a global `final class Env`. For Composer/PSR-4 projects, set `namespace=` and/or `class=` (e.g. `@generatePhpEnv(path=src/Env.php, namespace="App\\Config", class=AppEnv)`). ### `@generateTypes()` (deprecated) [Section titled “@generateTypes() (deprecated)”](#generatetypes-deprecated) Deprecated TypeScript-only alias for [`@generateTsTypes`](#generatetstypes). `@generateTypes(lang=ts, path=...)` still works; for other languages use the matching `@generate*Env` decorator (`@generateTypes(lang=py, ...)` errors with a pointer to `@generatePythonEnv`).