You deployed. An env var was missing. Prod crashed at 3am.
envguardr stops that before it happens — in any language, CI or deploy, without ever printing a value.

$ npx envguardr validate ./env.schema.yaml

❌ API_URL is required
❌ PORT must be less than or equal to 65535

Blocks bad deploys

Exits with 1 when the environment is invalid and 2 when it cannot validate. Any CI or deploy step can gate on it.

Any language

The schema is a YAML file. No JavaScript to write, nothing executed.

Never prints values

Errors name the variable and the rule, never the value. Safe to run next to secrets.

Strict by design

Rejects 1e5, yes, on. A typo in the schema fails instead of being ignored.

Machine-readable

--format json gives a stable contract for annotations, alerts or custom gates.

Supply chain transparency

Signed images, SBOM, and provenance — fully auditable end to end.

Quick start

Create an env.schema.yaml file:

version: 1

variables:
  API_URL:
    type: url
    required: true

  PORT:
    type: number
    default: 3000
    min: 1
    max: 65535

  NODE_ENV:
    type: enum
    values: [development, production, test]
    default: development

Validate the current environment:

npx envguardr validate ./env.schema.yaml

Or, without Node.js:

docker run --rm -v "$PWD/env.schema.yaml:/app/env.schema.yaml:ro,z" \
  -e API_URL -e PORT -e NODE_ENV \
  docker.io/fontebasso/envguardr:4 validate ./env.schema.yaml

envguardr only validates the environment of its own process. Load the variables any way you already do — CI secrets, docker run -e, an orchestrator — and run it there.

Install

npm install --save-dev envguardr
docker pull docker.io/fontebasso/envguardr:4

Schema

A schema has a version and a variables mapping. JSON files are accepted too.

version: 1

variables:
  API_KEY:
    type: string
    required: true
    pattern: '^[A-Za-z0-9]{32}$'
    message: API_KEY must be 32 alphanumeric characters

  CONFIG:
    type: string
    format: json

  DEBUG:
    type: boolean
    default: false

Types and constraints

Type Accepts Constraints
string Any string pattern, minLength, maxLength, format
number Decimal numbers; rejects 0xff, 1e5 min, max
boolean true or false only —
url Any scheme:// URL per RFC 3986, with international characters per RFC 3987: https://, postgres://, redis://, sqlite:///… —
enum One of values (required) —
Constraint Meaning
pattern RE2 regular expression, the same dialect as Go. Runs in linear time and supports Unicode classes like \p{L}; lookahead and backreferences are not supported. Not anchored: use ^...$ to match the whole value.
minLength, maxLength Inclusive, counted in Unicode characters.
format json (valid JSON text) or date (a real calendar date, YYYY-MM-DD).
min, max Inclusive.

Every variable also accepts:

Key Meaning
required true makes a missing or empty variable an error. Defaults to false.
default Value used when the variable is missing. Cannot be combined with required: true.
message Fixed text that replaces the error message for this variable.

Rules

  • A required variable must be set and not empty: VAR= fails with REQUIRED, which catches a CI secret that was never configured.
  • An optional variable set to empty (VAR=) is validated like any other value.
  • A missing optional variable without a default is not validated.
  • A default must satisfy the variable's own rules; otherwise the schema is invalid.
  • The schema is strict: unknown keys, constraints that do not belong to the type and invalid values are errors (exit 2), never ignored.
  • YAML 1.2 is used, so yes, no, on and off are plain strings.
  • A url must include scheme://, so localhost:8080 is rejected. Special characters in credentials must be percent-encoded (p%40ss for p@ss). To allow only some schemes, add a pattern such as '^https://'.
  • A number default is a YAML number: 0x10 means 16. Write it as a plain decimal; exponent forms such as 1e21 are rejected.
  • Anchors and aliases are allowed; merge keys (<<) are not.

Output and exit codes

Exit code Meaning
0 The environment is valid
1 The environment is invalid
2 Could not validate: usage error, missing or malformed schema, invalid schema, internal error

By default, problems go to stderr, one per line, and success to stdout. With --format json, a single JSON object goes to stdout:

{ "ok": true, "issues": [] }
{
  "ok": false,
  "issues": [
    { "key": "PORT", "code": "ABOVE_MAX", "message": "PORT must be less than or equal to 65535" }
  ]
}
{
  "ok": false,
  "error": { "code": "SCHEMA_INVALID", "message": "Unknown key 'requred'", "path": "variables.PORT" }
}
Issue code When
REQUIRED A required variable is missing
INVALID_NUMBER, INVALID_BOOLEAN, INVALID_URL, INVALID_ENUM The value does not match the type
PATTERN_MISMATCH pattern does not match
TOO_SHORT, TOO_LONG minLength / maxLength
BELOW_MIN, ABOVE_MAX min / max
INVALID_FORMAT format
Error code When
USAGE_ERROR Wrong command, option or arguments
SCHEMA_NOT_FOUND The schema file cannot be read
SCHEMA_PARSE_ERROR The file is not a single valid YAML document
SCHEMA_INVALID The schema breaks a rule above; path points to it
INTERNAL_ERROR Unexpected failure; no details are printed

Codes are part of the public contract and only change in a major version.

Docker

docker run --rm \
  -v "$PWD/env.schema.yaml:/app/env.schema.yaml:ro,z" \
  -e API_URL -e PORT \
  docker.io/fontebasso/envguardr:4 validate ./env.schema.yaml

Mount only the schema, read-only: the container never needs the rest of your project. The image runs as a non-root user (uid 65532) and works with --read-only and --cap-drop ALL. For production, pin the image by digest.

Platform Status
linux/amd64 Supported
linux/arm64 Supported

Recipes

envguardr has no integrations of its own. These are ways to run it where your variables already are.

GitHub Actions

- name: Validate environment
  run: npx envguardr@4 validate ./env.schema.yaml
  env:
    API_URL: ${{ vars.API_URL }}
    API_KEY: ${{ secrets.API_KEY }}

A secret that was never configured reaches the step as an empty string; required: true rejects it. Pin the major (envguardr@4) so a step that holds secrets never runs an unexpected version.

GitLab CI

validate-env:
  image: node:24
  script:
    - npx envguardr@4 validate ./env.schema.yaml

The envguardr image is distroless and has no shell, so it cannot run a script: block.

Kubernetes

Run it as an init container with the same variables as the app. If validation fails, the init container exits with 1, the app container never starts and the rollout does not progress. Values stay in the cluster: CI only needs to deploy, not to read secrets.

Store the schema in a ConfigMap:

kubectl create configmap env-schema --from-file=env.schema.yaml

Then add the init container, the volume and the same envFrom as the app to your Deployment:

spec:
  template:
    spec:
      initContainers:
        - name: envguardr
          image: docker.io/fontebasso/envguardr:4  # pin by digest in production
          args: ["validate", "/schema/env.schema.yaml"]
          envFrom:
            - configMapRef: { name: app-config }
            - secretRef: { name: app-secrets }
          volumeMounts:
            - { name: env-schema, mountPath: /schema, readOnly: true }
          securityContext:
            runAsNonRoot: true
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities: { drop: [ALL] }
      containers:
        - name: app
          # ...your app, with the same envFrom
      volumes:
        - name: env-schema
          configMap: { name: env-schema }

Read why a pod is waiting with kubectl logs <pod> -c envguardr: the output names the variables and rules, never the values.

.env files

envguardr does not read .env files. Load them with the tool you already use:

npx dotenv-cli -e .env.production -- npx envguardr@4 validate ./env.schema.yaml
docker run --rm --env-file .env.production \
  -v "$PWD/env.schema.yaml:/app/env.schema.yaml:ro,z" \
  docker.io/fontebasso/envguardr:4 validate ./env.schema.yaml

Docker's --env-file keeps quotes as part of the value: API_URL="https://api.example.com" reaches envguardr with the quotes and fails as an invalid URL. Write the file without quotes for Docker, or load it with dotenv-cli, which removes them. Since envguardr never prints values, check the file itself when a value that looks right is rejected.

Migrating from v3

v3 (env.schema.js) v4 (env.schema.yaml)
type: { enum: [...] } type: enum + values: [...]
validators.regex(/re/) pattern: 're' (RE2 syntax: rewrite lookahead and backreferences)
validators.regex(/re/i), /m, /s pattern: '(?i)re', (?m), (?s): flags go at the start of the pattern
validators.range(a, b) min: a, max: b
validators.oneOf([...]) type: enum + values: [...]
validators.json() format: json
validators.date('YYYY-MM-DD') format: date
validators.awsArn(...) pattern: '^arn:aws:...'
validators.all(a, b, ...) Several constraints on the same variable: all must pass, and the first failure is reported
type: 'custom' with a function No equivalent: use pattern, or stay on v3

For example, this v3 schema:

API_KEY: {
  type: 'custom',
  validator: validators.all(
    validators.regex(/^sk_[a-z0-9]+$/i),
    validators.regex(/^.{20,40}$/),
  ),
  required: true,
},

becomes:

API_KEY:
  type: string
  required: true
  pattern: '(?i)^sk_[a-z0-9]+$'
  minLength: 20
  maxLength: 40

Write the pattern without the JavaScript slashes: '/^a$/i' is a valid pattern that looks for the literal text /a/i and will reject your values. There is no global flag (g); it has no meaning when validating a single value.

Other changes:

  • The schema is data, not code: envguardr no longer imports a JavaScript file.
  • Exit code 2 now means "could not validate"; 1 only means an invalid environment.
  • The Docker latest tag points to v4. Pin docker.io/fontebasso/envguardr:3 to stay on v3.

v3 receives security fixes only, for six months after the release of v4.0.0. Its documentation stays at v3.

Security & supply chain

  • Environment values are never printed, in any output format or error
  • The schema is never executed
  • npm Trusted Publishing with provenance (OIDC-based, no long-lived tokens)
  • Docker images with provenance and SBOM
  • Signed images via cosign / Sigstore (keyless, OIDC)
  • Distroless, non-root containers
  • GitHub Actions pinned by SHA
  • CodeQL scanning enabled

Verifying container images

cosign verify \
  --certificate-identity-regexp "https://github.com/fontebasso/envguardr/.github/workflows/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  docker.io/fontebasso/envguardr:latest

To report a vulnerability, see SECURITY.md.