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 withREQUIRED, 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
defaultis not validated. - A
defaultmust 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,onandoffare plain strings. - A
urlmust includescheme://, solocalhost:8080is rejected. Special characters in credentials must be percent-encoded (p%40ssforp@ss). To allow only some schemes, add apatternsuch as'^https://'. - A
numberdefault is a YAML number:0x10means16. Write it as a plain decimal; exponent forms such as1e21are 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
2now means "could not validate";1only means an invalid environment. - The Docker
latesttag points to v4. Pindocker.io/fontebasso/envguardr:3to 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.