Config Item @decorators
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).
# @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.
Built-in item decorators
Section titled “Built-in item decorators”These are the item decorators built into Varlock. Plugins can extend the DSL with additional decorators.
@required
Section titled “@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.
💡 Use the forEnv() function to set required based on the current environment.
# @defaultRequired=false# ---# @required # same as @required=trueREQUIRED_ITEM=
# @required=forEnv(prod)REQUIRED_FOR_PROD_ITEM=
# @required=eq($OTHER, foo)REQUIRED_IF_OTHER_IS_FOO=@optional
Section titled “@optional”Value type: boolean
Opposite of @required. Equivalent to writing @required=false.
# @defaultRequired=true# ---# @optionalOPTIONAL_ITEM=@sensitive
Section titled “@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
Sensitivity may also be implied by the item’s data type. For example, plugin “secret-zero” credential types (like a 1Password opServiceAccountToken) 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.
Redaction replaces a sensitive value wherever it appears, and it can’t tell a leaked secret from ordinary text that happens to match. Varlock therefore checks that a value marked sensitive can actually be protected that way, and reports the ones that can’t be: very short values, booleans and numbers, the @currentEnv item, and secrets embedded in a non-sensitive value. See values varlock can’t protect for what each case means and how to resolve it.
📖 See the secrets management guide for best practices on handling sensitive values, and how to use plugins to fetch them from secret management platforms.
# @sensitiveSERVICE_X_PRIVATE_KEY=# @sensitive=falseSERVICE_X_CLIENT_ID=Per-item options: pass an object value to tune protection for a single sensitive item:
enabled(boolean, defaulttrue): 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, defaulttrue): whenfalse, this item’s value is excluded from leak detection, 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.redact(boolean, defaulttrue): whenfalse, this item’s value is left out of log redaction (console, stdout/stderr,varlock runoutput, andredactSensitiveConfig()). Useful when a program is meant to print the value, like a script whose output is captured withTOKEN=$(node print-token.js). Leak detection still applies, including scrubbing it from responses.redactLogsis accepted as a deprecated alias of this option.allowShortValue(boolean, defaultfalse): whentrue, silences the short-value warning for secrets that are legitimately short and can’t be lengthened (a one-time code, a PIN). Redaction is unchanged; this only records that the collision risk was read and accepted. It does not apply to values under 3 characters: those are an error when you wrote@sensitiveyourself and a warning when@defaultSensitiveswept them in, and the option suppresses neither.
# @sensitive={preventLeaks=false}TOKEN_FORWARDED_TO_PARTNER=
# printed on purpose, so not redacted from output# @sensitive={redact=false}DEPLOY_TOKEN=
# sensitive only in production, and allowed to leave the system# @sensitive={enabled=forEnv(production), preventLeaks=false}PARTNER_TOKEN=@public
Section titled “@public”Value type: boolean
Opposite of @sensitive. Equivalent to writing @sensitive=false.
# @defaultSensitive=true# ---# @publicPUBLIC_API_URL=https://api.example.com@internal
Section titled “@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() or string expansion), but they are excluded from everything that reaches your app: the injected env vars, the serialized graph blob, and generated types. Framework integrations (Vite, Next.js, Astro, Cloudflare) inherit this exclusion too, since they all source their config from the same CLI output.
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.
# the token below is only used to resolve the secrets that follow it# @internal @sensitiveOP_TOKEN=
# resolved using OP_TOKEN, but only DATABASE_URL is injected into the app# @sensitiveDATABASE_URL=exec(`op read "op://app/db/url"`)Some plugin data types are internal by default. For example, a 1Password opServiceAccountToken or other secrets-manager credential authenticates varlock but is rarely needed in app code. You can opt back in with @internal=false:
# @type=opServiceAccountToken @internal=falseOP_TOKEN=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 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 there for local debugging.
@dynamic
Section titled “@dynamic”Value type: boolean | boot
Sets whether the item is dynamic - meaning integrations should avoid replacing it at build time and instead resolve it at runtime.
By default, dynamic behavior follows sensitivity (sensitive items are dynamic, non-sensitive items are static), but this can be overridden globally with @defaultDynamic.
@dynamic=boot marks a value that is bound at process start on each instance, such as a platform-assigned PORT. It only changes behavior under varlock freeze, where the frozen value becomes a default the environment can override at boot. Everywhere else it behaves like @dynamic, and most schemas never need it. Boot items have restrictions (built-in type, nothing may reference them, not @internal), listed in the guide.
Items can also be selected by this state via the @dynamic selector (or !@dynamic for static items) in the --filter language. See the Static vs Dynamic Config guide for the full model.
# @dynamicPUBLIC_RUNTIME_FLAG=
# @type=port @dynamic=bootPORT=3000@static
Section titled “@static”Value type: boolean
Opposite of @dynamic. Equivalent to writing @dynamic=false.
# @staticSENSITIVE_BUILD_TIME_TOKEN=Value type: data type (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 for more details.
If not specified, a data type will be inferred when possible, or default to string otherwise.
# @type=url # name onlySOME_URL=
# @type=string(startsWith=abc) # function call with optionsEXAMPLE_WITH_TYPE_OPTIONS=
INFER_NUMBER=123 # data type of `number` will be inferred from the value@example
Section titled “@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.
# @example="sk-abc123"SECRET_KEY=@docs()
Section titled “@docs()”Arg types: [ url: string ] | [ description: string, url: string ]
URL of documentation related to the item. Will be included in generated types. Can be called multiple times.
# @docs(https://xyz.com/docs/api-keys)# @docs("Authentication guide", https://xyz.com/docs/auth-guide)XYZ_API_KEY=
example of docs() info in generated types / IntelliSense
@tag()
Section titled “@tag()”Arg types: [ tag: string, ... ]
Attaches one or more tags to the item. Tags don’t affect resolution or validation; they’re used to select items with the --filter CLI flag on varlock load/varlock run, with pick/omit on @import() (e.g. @import(../../, pick=[#billing])), and with the filter= arg on @generate* code-generation decorators (e.g. --filter="#billing", @generateTsTypes(filter=#billing)). Can be called multiple times; tags from every call accumulate (duplicates collapse).
Tag names must start with a letter or number, followed by letters, numbers, _, or -. This keeps every tag selectable with a #tagname filter, which reserves characters like ,, !, and *. An invalid tag name is a schema error.
# @tag(billing)# @tag(prod, critical)STRIPE_SECRET_KEY=varlock load --filter="#billing"@docsUrl (deprecated)
Section titled “@docsUrl (deprecated)”Value type: string
URL of documentation related to the item.
Use @docs() instead, which supports multiple docs entries with optional descriptions.
@docsUrl=https://example.com -> @docs(https://example.com)
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. Custom icons are fetched from Iconify when types are generated (and left out if the fetch fails); set icons=false on @generateTsTypes if the generated file must not depend on network access.
See https://icones.js.org/ to browse available icons.
Useful for generated docs and UI surfaces that show schema metadata.
# @icon=mdi:keySERVICE_X_API_KEY=@deprecated
Section titled “@deprecated”Value type: boolean | string
Marks an item as deprecated. Does not affect resolution or validation: the item still resolves and stays valid. Use this when a variable should keep working during a migration, but callers should move off it.
- bare
@deprecatedor@deprecated=true: marks the item deprecated (no message) @deprecated="message": marks it deprecated with a message (for example, what to use instead)@deprecated=false: explicitly not deprecated
Effects:
- Pretty CLI output (e.g.
varlock load): the key is struck through and a deprecated badge is shown - Generated types: TypeScript gets a
@deprecatedJSDoc tag (with the message when set); other language emitters include aDeprecated:doc line
# @deprecated # same as @deprecated=trueLEGACY_API_URL=
# @deprecated="Use NEW_API_URL instead"OLD_API_URL=
# @deprecated=falseSTILL_SUPPORTED=@auditIgnore
Section titled “@auditIgnore”Value type: boolean
Suppresses “unused in schema” warnings from varlock audit for this item. Useful for items that are only consumed by external tools and won’t appear in your application code. For the opposite case, a key the scanner finds in code that should not be in the schema, use @auditIgnoreKeys().
# @auditIgnore# used by CI tooling, not referenced in codeCI_DEPLOY_TOKEN=@placeholder
Section titled “@placeholder”Value type: string
Sets an explicit placeholder string for the item, used in place of the real value when the item is routed through the credential proxy. A placeholder is what an untrusted child process (e.g. an AI agent) sees instead of the secret.
Without an explicit @placeholder, varlock derives one from the item’s @type format, falling back to a generic value. Set this when a generic placeholder would fail a client’s key-format validation (e.g. an sk-… prefix check).
# @proxy(domain="api.openai.com")# @placeholder=sk-proj-000000000000000000000000OPENAI_API_KEY=yourPreferredPlugin()@proxy
Section titled “@proxy”Value type: function @proxy(domain=..., ...) or value @proxy=passthrough|omit
Routes an item’s secret through the credential proxy so an untrusted child process only ever sees a placeholder, while the real value is injected into matching outbound requests at the network boundary. Using @proxy(...) on an item implies @sensitive.
Function form @proxy(domain=..., [path], [method], [block], [approval], [keys], [substituteIn], [rules], [transform]):
| Option | Meaning |
|---|---|
domain | (required) Host to match: a single host or an array ([a.com, b.com]); supports globs (*.example.com). |
path | Restrict to matching URL paths (glob), e.g. "/v1/**". |
method | Restrict to one or more HTTP methods, e.g. [GET, POST]. |
block | block=true denies matching requests outright. |
approval | approval=true holds matching requests for an interactive yes/no in the proxy start terminal before they proceed. A self-contained one-shot proxy run has no terminal to prompt in and denies them. |
keys | Array of additional item names to inject for this rule, e.g. keys=[OTHER_KEY]. |
substituteIn | Where the placeholder may be swapped for the real value: header (default), header:<name>, path, query, query:<param>, or body:<path>, e.g. [header, "body:client_secret"]. Body always requires a path (body:* allows anywhere, for bodies that can’t be parsed into one). Each target is worth one substitution per request. A placeholder outside every target is skipped: forwarded unsubstituted and audited. A repeat at one target, or an occurrence off the named path/param inside a targeted body or query, blocks the request. See Substitution surface. |
rules | Array of policy refinements sharing this rule’s domain, e.g. rules=[{path="/v1/**", block=true}]. Each entry may set path/method/block/approval/substituteIn (not domain/keys) and injects nothing on its own. See the Grouping rules guide. |
transform | Request transform config, e.g. transform={scheme="hmac-sha256", stringToSign="{timestamp}{method}{path}{body}", signatureHeader="X-Signature"}. The proxy computes the credential for the final outbound request rather than substituting a placeholder. Built-in schemes: hmac-sha256, hmac-sha512, and http-basic (which composes the Authorization: Basic header the proxy sends, since base64 hides a placeholder from substitution); plugins add more, e.g. aws-sigv4. Credential items are always named with $ITEM references (secretKey=$PARTNER_SECRET, or username/password for http-basic), never inline values; on an attached rule the decorated item supplies the credential by default. See Request transforms for per-scheme options. |
The same decorator in the header creates a detached policy rule (no injection unless it lists keys).
Value form @proxy=passthrough injects the real value into the child (escape hatch); @proxy=omit explicitly withholds it. The value and function forms are mutually exclusive on one item.
# @proxy(domain="api.stripe.com", path="/v1/**")STRIPE_SECRET_KEY=yourPreferredPlugin()
# @sensitive# @proxy=passthroughLEGACY_TOKEN=yourPreferredPlugin()See the credential proxy guide for the full workflow.