| name | deployment-validation-config-validate |
| version | 1.2.1 |
| description | Validates deploy-time config as a typed contract: Ajv JSON Schema 2020-12, environment policy (debug, HTTPS, password length, encryption-at-rest), and YAML/JSON/TOML/INI/.env drift plus secret-shape checks. Trigger on CI config gates or failing the build when production debug is enabled. Do not use for OPA/RBAC runtime policy. Never treat this folder as a shipped analyzer or validator file tree; helpers are inlined here. |
| risk | safe |
| source | openrouter-deepsearch |
| date_added | 2026-06-16T00:00:00.000Z |
Configuration Validation
Treat configuration as a verified contract rather than a loose collection of files: every value should have a declared shape, every environment should enforce its own safety policy, and every secret should be encrypted at rest. This skill builds that contract layer by layer — discovery, schema validation, environment policy, tests, runtime re-validation, versioned migrations, encryption, and generated docs.
When to Use
- Validating config before deploy. A bad value (a
port of 70000, debug: true in production) should fail CI, not page someone at 2am. Schema + environment validation catches these deterministically.
- Building CI/CD validation for YAML, JSON, TOML, INI,
.env, or JS config files. Each format parses to the same in-memory shape, so one schema can guard them all.
- Enforcing environment-specific security and compliance rules. Structural validity is not the same as "safe for production"; the environment validator encodes that gap.
- You need a checklist or reference implementation. The sections below are working, typed reference code you can adapt rather than rules to memorize.
Do not use when:
- The task is unrelated to configuration validation. Reach for a domain-appropriate skill instead.
- You actually need runtime policy enforcement (RBAC, quotas, network policy). This skill validates shape and values; it does not replace policy engines like OPA (Open Policy Agent). Use OPA when decisions depend on external state rather than the config file itself.
- Secrets are in plaintext config and you have no plan to encrypt or externalize them. This skill assumes secrets are either encrypted or pulled from a secret manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault).
Prerequisites
Procedure
Step 1 — Configuration Analysis (Discovery & Drift Detection)
Why: You cannot validate what you have not inventoried. Before writing a single schema, discover every config file, flag values that look like inlined secrets, and detect drift (a key present in staging but missing in production is one of the most common deploy-time surprises).
Copy the analyzer below into the application repo as a local helper (for example config_analyzer.py). The listing in this file is the canonical version.
from __future__ import annotations
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import yaml
@dataclass(frozen=True)
class ConfigFile:
path: str
type: str
environment: str
@dataclass(frozen=True)
class SecurityIssue:
file: str
type: str
severity: str
detail: str
@dataclass(frozen=True)
class ConsistencyIssue:
files: List[str]
type: str
detail: str
@dataclass
class ProjectAnalysis:
config_files: [ConfigFile] = field(default_factory=)
security_issues: [SecurityIssue] = field(default_factory=)
consistency_issues: [ConsistencyIssue] = field(default_factory=)
recommendations: [] = field(default_factory=)
:
IGNORE_DIRS = ({, , , , , , })
SECRET_PATTERNS: [, ] = {
: re.(, re.IGNORECASE),
: re.(, re.IGNORECASE),
: re.(, re.IGNORECASE),
: re.(, re.IGNORECASE),
}
REAL_SECRET_VALUE = re.(
)
PLACEHOLDER_TOKENS = (, , , , , , )
() -> :
root = Path(project_path)
root.is_dir():
NotADirectoryError()
.root = root
() -> ProjectAnalysis:
config_files = ._find_config_files()
ProjectAnalysis(
config_files=config_files,
security_issues=._check_security_issues(config_files),
consistency_issues=._check_consistency(config_files),
recommendations=._build_recommendations(config_files),
)
() -> [ConfigFile]:
patterns = [, , , , , , , ]
found: [, ConfigFile] = {}
pattern patterns:
path .root.rglob(pattern):
path.is_file() ._should_ignore(path):
resolved = (path)
found[resolved] = ConfigFile(
path=resolved,
=._detect_config_type(path),
environment=._detect_environment(path),
)
(found.values(), key= c: c.path)
() -> :
(part .IGNORE_DIRS part path.parts)
() -> :
name = path.name.lower()
name:
{
: ,
: ,
: ,
: ,
: ,
: ,
}.get(path.suffix.lower(), )
() -> :
name = path.name.lower()
aliases = {: , : , : }
token (, , , , , , , ):
token name:
aliases.get(token, token)
() -> [SecurityIssue]:
issues: [SecurityIssue] = []
config config_files:
:
content = Path(config.path).read_text(encoding=)
(OSError, UnicodeDecodeError):
label, pattern .SECRET_PATTERNS.items():
pattern.finditer(content):
line = ._line_at(content, .start())
._looks_like_real_secret(line):
issues.append(SecurityIssue(
file=config.path,
=,
severity=,
detail=,
))
issues
() -> :
lowered = line.lower()
(token lowered token .PLACEHOLDER_TOKENS):
(.REAL_SECRET_VALUE.search(line))
() -> :
start = content.rfind(, , index) +
end = content.find(, index)
content[start:] end == - content[start:end]
() -> [ConsistencyIssue]:
issues: [ConsistencyIssue] = []
by_type: [, [ConfigFile]] = {}
config config_files:
by_type.setdefault(config., []).append(config)
config_type, group by_type.items():
(group) < :
key_sets: [, []] = {}
config group:
keys = ._top_level_keys(Path(config.path))
keys :
key_sets[config.path] = keys
(key_sets) < :
union: [] = ().union(*key_sets.values())
path, keys key_sets.items():
missing = union - keys
missing:
issues.append(ConsistencyIssue(
files=[path],
=,
detail=,
))
issues
() -> [[]]:
suffix = path.suffix.lower()
:
text = path.read_text(encoding=)
(OSError, UnicodeDecodeError):
:
suffix == :
data: = json.loads(text)
suffix (, ):
data = yaml.safe_load(text)
:
(json.JSONDecodeError, yaml.YAMLError):
(data.keys()) (data, )
() -> []:
recommendations: [] = []
config_files:
recommendations.append()
recommendations
(c. == c config_files):
recommendations.append()
(c.environment == c config_files):
recommendations.append(
)
recommendations.append()
recommendations
Run the analyzer on Windows (PowerShell):
python -c "from config_analyzer import ConfigurationAnalyzer; import json; r = ConfigurationAnalyzer('~\myproject').analyze_project(); print(json.dumps({'files': len(r.config_files), 'security': len(r.security_issues), 'consistency': len(r.consistency_issues)}, indent=2))"
Step 2 — Schema Validation (JSON Schema 2020-12 with Ajv)
Why: A schema is the single source of truth for a config's shape. Define it once and validate every format against it. Three constructor choices matter:
strict: true — makes a typo in the schema a hard error instead of a silent no-op.
coerceTypes: true — turns the "5432" you get from an env var or .env parser into the number 5432.
useDefaults: true — fills in declared defaults so downstream code never sees undefined.
Typing the schema as JSONSchemaType<DatabaseConfig> means the schema and the TypeScript interface can never drift apart without a compile error.
Copy the validator class and schema types below into the application repo (for example config-validator.ts and schemas.ts). The listing in this file is canonical.
import Ajv2020 from "ajv/dist/2020";
import type { DefinedError, JSONSchemaType, SchemaObject, ValidateFunction } from "ajv";
import addFormats from "ajv-formats";
export interface ValidationError {
readonly path: string;
readonly message: string;
readonly keyword: string;
}
export interface ValidationResult {
readonly valid: boolean;
readonly errors: readonly ValidationError[];
}
export class ConfigValidator {
private readonly ajv: Ajv2020;
private readonly compiled = new Map<string, ValidateFunction>();
() {
. = ({
: ,
: ,
: ,
: ,
});
(.);
.();
}
(): {
..(, {
: ,
: (: ): {
{
(value). === ;
} {
;
}
},
});
..(, {
: ,
: (: ): .(value) && value >= && value <= ,
});
..(, {
: ,
: (: ): .(value),
});
}
(: , : ): {
: ;
{
validateFn = .(schema);
} (error) {
message = error ? error. : (error);
{ : , : [{ : , : , : }] };
}
((configData)) {
{ : , : [] };
}
errors = (validateFn. ?? []) [];
{
: ,
: errors.((error): ({
: error.. > ? error. : ,
: error. ?? ,
: error.,
})),
};
}
(: ): {
key = schema. === ? schema. : .(schema);
cached = ..(key);
(cached) {
cached;
}
fn = ..(schema);
..(key, fn);
fn;
}
}
{
: ;
?: ;
}
{
: ;
: ;
: ;
: ;
: ;
?: ;
}
: SchemaType<> = {
: ,
: ,
: ,
: {
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: {
: ,
: {
: { : },
: { : , : },
},
: [],
: ,
: ,
},
},
: [, , , , ],
: ,
};
Step 3 — Environment-Specific Validation
Why: A config can be structurally perfect and still be unsafe to deploy. debug: true is helpful locally and dangerous in production; an http:// URL is fine against localhost and a data-leak waiting to happen in staging. Schema validation answers "is this the right shape?"; environment validation answers "is this safe here?". Keeping the two layers separate means the schema stays reusable while each environment tightens the screws independently.
Copy the environment validator below into the application repo as a local helper (for example environment_validator.py).
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List, Mapping, Tuple, TypedDict
class EnvironmentRule(TypedDict, total=False):
allow_debug: bool
require_https: bool
min_password_length: int
require_encryption: bool
@dataclass(frozen=True)
class Violation:
rule: str
message: str
severity: str
class EnvironmentValidator:
def __init__(self) -> None:
self.environment_rules: Dict[str, EnvironmentRule] = {
"development": {"allow_debug": True, "require_https": False, "min_password_length": 8},
"staging": {"allow_debug": False, "require_https": True, "min_password_length": 12},
"production": {
: ,
: ,
: ,
: ,
},
}
() -> [Violation]:
environment .environment_rules:
ValueError(
)
(config, Mapping):
TypeError()
rules = .environment_rules[environment]
violations: [Violation] = []
rules.get(, ) (config.get(, )):
violations.append(Violation(
rule=,
message=,
severity=,
))
rules.get(, ):
path, url ._extract_urls(config):
url.startswith() ._is_loopback(url):
violations.append(Violation(
rule=,
message=,
severity=,
))
min_len = rules.get()
(min_len, ):
password = config.get(, )
(password, ) < (password) < min_len:
violations.append(Violation(
rule=,
message=,
severity=,
))
rules.get(, ) ._secrets_encrypted(config):
violations.append(Violation(
rule=,
message=,
severity=,
))
violations
() -> [[, ]]:
urls: [[, ]] = []
() -> :
(value, Mapping):
key, child value.items():
recurse(child, path (key))
(value, (, )):
index, child (value):
recurse(child, )
(value, ) (value.startswith() value.startswith()):
urls.append((path, value))
recurse(config, )
urls
() -> :
(host url host (, , ))
() -> :
password = config.get()
(password, Mapping):
password.get()
password
Environment policy summary (HARD RULES):
| Environment | debug | HTTPS required | Min password length | Encryption at rest |
|---|
| development | allowed | no | 8 | no |
| staging | blocked | yes | 12 | no |
| production | blocked | yes | 16 | yes |
Step 4 — Configuration Testing (Jest v30)
Why: Schemas are code, and code regresses. Pin both the happy path and the specific failure modes you care about, so a future schema edit that accidentally loosens the port range or drops a required field fails a test instead of shipping. Asserting on keyword and path (rather than just valid === false) ties each test to the reason it should fail, which keeps the tests meaningful as the schema evolves.
Copy the Jest examples below into the application repo (for example config-validator.test.ts) and pin both the happy path and the failure keyword you care about.
import { describe, it, expect, beforeEach } from "@jest/globals";
import { ConfigValidator } from "./config-validator";
import { databaseSchema, type DatabaseConfig } from "./schemas";
describe("ConfigValidator", () => {
let validator: ConfigValidator;
beforeEach(() => {
validator = new ConfigValidator();
});
const baseConfig: DatabaseConfig = {
host: "db.example.com",
port: 5432,
database: "myapp",
user: "dbuser",
password: "StrongPass!2024",
ssl: { enabled: true },
};
it("accepts a well-formed database config", () => {
const result = validator.validate(baseConfig, databaseSchema);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength();
});
(, {
result = validator.({ ...baseConfig, : }, databaseSchema);
(result.).();
(result..( e. === && e..())).();
});
(, {
: <> = {
: ,
: ,
: ,
: ,
: { : },
};
result = validator.(incomplete, databaseSchema);
(result.).();
(result..( e. === && e..())).();
});
(, {
result = validator.({ ...baseConfig, : }, databaseSchema);
(result.).();
(result..( e..() && e. === )).();
});
});
Run tests on Windows (PowerShell):
npx jest --config jest.config.ts --verbose
Step 5 — CI/CD Integration
Wire schema validation and environment validation into CI so a bad config never reaches deployment.
Copy the inlined helpers into the application repo, then fail CI on any of: analyzer high-severity hits, schema valid === false, environment violations at critical/high, or Jest failures.
# Analyzer (copied helper lives in the app repo, not this skill folder)
python -c "from config_analyzer import ConfigurationAnalyzer; import sys; r = ConfigurationAnalyzer('.').analyze_project(); print(len(r.security_issues), len(r.consistency_issues)); sys.exit(1 if r.security_issues else 0)"
# Schema + environment checks: invoke the copied ConfigValidator / EnvironmentValidator the same way as Verification below.
# Pin schema failure keywords
npx jest --config jest.config.ts
HARD RULE: Every step above must exit non-zero on failure. A passing CI run with a debug: true in production config is a bug in the pipeline, not a feature.
Pitfalls
-
Ajv with strict: false and no explicit overrides. Strict mode is what catches typos in the schema itself (an unknown keyword silently does nothing in loose mode). The loose default is deprecated and slated for removal in Ajv v9, so write schemas that pass strict mode now rather than migrating under pressure later. Always use strict: true.
-
Secrets in plaintext config. Even in a private repo, plaintext secrets leak through backups, logs, and CI artifacts. Encrypt them or pull them from a secret manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault). The skill assumes secrets are either encrypted or externalized. Never commit a real API key — use YOUR_KEY placeholders in examples.
-
Forgetting coerceTypes when env vars feed the schema. Environment variables and .env/INI parsers yield strings. Without coerceTypes: true, a port: "5432" from an env var will fail an integer type check even though the value is semantically correct.
-
Not caching compiled schemas by $id. Ajv refuses to compile the same $id twice. The ConfigValidator.compile() method caches by $id (falling back to serialized schema for anonymous schemas). If you skip caching, repeated validation calls will throw.
-
Only checking the first error. Use allErrors: true so the validator reports every problem at once. A single-error report means the developer fixes one issue, re-runs, and discovers the next — a slow feedback loop that discourages thorough fixes.
-
Asserting only valid === false in tests. Assert on keyword and path to tie each test to the reason it should fail. A test that only checks valid === false will still pass if the schema accidentally rejects for the wrong reason, masking regressions.
-
Mixing schema validation and environment policy in one layer. Schema validation answers "is this the right shape?"; environment validation answers "is this safe here?". Combining them makes the schema non-reusable across environments and makes policy changes require schema edits. Keep them separate.
-
Ignoring config drift between environments. A key present in but missing in is one of the most common deploy-time surprises. The analyzer's method compares top-level key sets of same-type configs across environments — always run it before deploy.
Verification
-
Analyzer runs and reports findings:
python -c "from config_analyzer import ConfigurationAnalyzer; r = ConfigurationAnalyzer('~\myproject').analyze_project(); print(f'Files: {len(r.config_files)}, Security: {len(r.security_issues)}, Consistency: {len(r.consistency_issues)}')"
Expected output: Files: N, Security: 0, Consistency: 0 (or non-zero counts that you investigate and resolve).
-
Schema validation passes for a valid config:
npx tsx -e "import { ConfigValidator } from './config-validator'; import { databaseSchema } from './schemas'; const v = new ConfigValidator(); const r = v.validate({host:'db.example.com',port:5432,database:'myapp',user:'dbuser',password:'StrongPass!2024',ssl:{enabled:true}}, databaseSchema); console.log(JSON.stringify(r, null, 2));"
Expected output: {"valid": true, "errors": []}
-
Schema validation rejects an invalid port:
npx tsx -e "import { ConfigValidator } from './config-validator'; import { databaseSchema } from './schemas'; const v = new ConfigValidator(); const r = v.validate({host:'db.example.com',port:70000,database:'myapp',user:'dbuser',password:'StrongPass!2024',ssl:{enabled:true}}, databaseSchema); console.log(r.valid, r.errors[0]?.keyword);"
Expected output: false format
-
Environment validator blocks debug: true in production:
python -c "from environment_validator import EnvironmentValidator; v = EnvironmentValidator(); r = v.validate_config({'debug': True, 'password': {'encrypted': True, 'ciphertext': '...'}}, 'production'); print([(x.rule, x.severity) for x in r])"
Expected output: [('no_debug_outside_dev', 'critical')]
-
Jest test suite passes:
npx jest --config jest.config.ts --verbose
Expected: all tests pass, 0 failures.
-
No inlined secrets detected by analyzer:
python -c "from config_analyzer import ConfigurationAnalyzer; r = ConfigurationAnalyzer('~\myproject').analyze_project(); print([s.detail for s in r.security_issues])"
Expected output: [] (empty list — no inlined secrets found).
Related Skills
- secrets-management — for encrypting secrets at rest and integrating with external secret managers.
- ci-cd-pipeline-validation — for wiring config validation into broader CI/CD pipelines.
- policy-as-code — for runtime policy enforcement with OPA when decisions depend on external state.