env-drift-check — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited env-drift-check (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Environment drift is the gap that opens slowly between the four sources of truth for how a project runs: what variables are declared, what runtime the lockfile was resolved against, what the CI matrix specifies, and what the Docker image actually contains. Each source drifts independently. This skill reads all four, cross-references them, and reports concrete mismatches — not generic advice about using .env.example.
This is distinct from environment setup (use ci-cd-pipeline for that). This skill detects drift that has accumulated in a working project over time.
Run these in parallel to map the environment landscape before checking for drift:
# Variable declarations
ls .env.example .env.sample .env.template .env.defaults 2>/dev/null
# Lockfiles and runtime manifests
ls package-lock.json yarn.lock pnpm-lock.yaml poetry.lock Pipfile.lock Gemfile.lock Cargo.lock go.sum 2>/dev/null
# Runtime version declarations
cat .nvmrc 2>/dev/null
cat .node-version 2>/dev/null
cat .python-version 2>/dev/null
cat .ruby-version 2>/dev/null
cat .tool-versions 2>/dev/null # asdf
cat .mise.toml 2>/dev/null # mise
# CI configuration
ls .github/workflows/*.yml .github/workflows/*.yaml .gitlab-ci.yml .circleci/config.yml .buildkite/pipeline.yml bitbucket-pipelines.yml 2>/dev/null
# Container
ls Dockerfile docker-compose.yml docker-compose.yaml compose.yml 2>/dev/nullIf none of these exist, say: "No environment artefacts found. This project may not have formalised environment configuration yet — consider adding a .env.example and pinning your runtime version."
#### 2a. Extract declared keys from .env.example (or equivalent)
grep -v "^\s*#" .env.example | grep -v "^\s*$" | cut -d= -f1 | sort#### 2b. Extract keys referenced in application code
# Node/JS/TS
grep -rE "process\.env\.[A-Z_]+" --include="*.js" --include="*.ts" --include="*.mjs" src/ app/ lib/ 2>/dev/null | grep -oE "process\.env\.[A-Z_]+" | sort -u | sed 's/process\.env\.//'
# Python
grep -rE "os\.environ(\[|\.get)\[?['\"]([A-Z_]+)" --include="*.py" . 2>/dev/null | grep -oE "['\"][A-Z_]+" | tr -d "'" | tr -d '"' | sort -u
# Go
grep -rE 'os\.Getenv\("[A-Z_]+"\)' --include="*.go" . 2>/dev/null | grep -oE '"[A-Z_]+"' | tr -d '"' | sort -u#### 2c. Cross-reference
.env.example but not referenced in code → STALE (probably safe to remove).env.example → UNDOCUMENTED (missing from onboarding guide; breaks new dev setup)#### 3a. Read the declared runtime version
Sources, in priority order:
| File | How to read |
|---|---|
.nvmrc / .node-version | Read directly — single version string |
.python-version | Read directly |
.tool-versions (asdf) | Parse node X.Y.Z or python X.Y.Z lines |
.mise.toml | Parse [tools] section |
package.json → engines.node | Parse "node": ">=20" |
pyproject.toml → [tool.poetry.dependencies] | Parse python = "^3.11" |
#### 3b. Read the lockfile-implied runtime
# Node — check the lockfile format version (implies minimum Node version)
head -5 package-lock.json 2>/dev/null # "lockfileVersion": 3 → Node ≥18
# Python — check requires-python from poetry.lock or Pipfile.lock
grep "python_requires\|python-requires\|requires_python" poetry.lock Pipfile.lock 2>/dev/null | head -5#### 3c. Read the CI matrix runtime
cat .github/workflows/*.yml 2>/dev/null | grep -E "node-version|python-version|ruby-version|java-version" | head -20#### 3d. Read the Docker base image runtime
grep "^FROM" Dockerfile 2>/dev/nullMap the image tag to a runtime version:
node:22-alpine → Node 22python:3.12-slim → Python 3.12node:lts → resolve LTS at time of last pull (flag as imprecise — prefer a pinned version)#### 3e. Cross-reference all four sources
Produce a version matrix:
| Source | Version |
|---|---|
.nvmrc | 22.3.0 |
package.json engines | >=20 |
| CI matrix | 20, 22 |
| Docker base | node:lts (imprecise) |
Flag any mismatch where a fixed version in one source falls outside the range or differs from another fixed version.
For each CI workflow file found, extract env vars passed to the job:
grep -E "^\s+(env:|[A-Z_]+:)" .github/workflows/*.yml 2>/dev/null | grep -v "^\s*#"Cross-reference against .env.example keys:
.env.example marked as required (no default value set, i.e. KEY= with empty value) but absent from any CI env: block or secrets reference → CI GAP: these jobs may fail silently if the key is needed at runtimeenv: that are not in .env.example → CI-ONLY KEY: document it or add it to .env.exampleIf a Dockerfile exists and a lockfile exists:
# Does the Dockerfile copy the lockfile and use it?
grep -E "COPY.*lock|npm ci|pip install --require-hashes|poetry install --no-root" Dockerfile 2>/dev/nullnpm install (not npm ci) in Dockerfile → DRIFT RISK: ignores lockfile, installs latest matching semverpip install -r requirements.txt without a hash-pinned lockfile → DRIFT RISKnpm ci / equivalent → DRIFT RISKnpm ci or poetry install used with lockfile copied → PASSOutput in this format:
## Environment Drift Report — <project-name>
### A. Variable Key Drift
| Key | Status | Action |
|-----|--------|--------|
| DATABASE_URL | ✅ Declared + used | — |
| STRIPE_WEBHOOK_SECRET | ⚠️ UNDOCUMENTED | Add to .env.example |
| OLD_REDIS_URL | ⚠️ STALE | Remove from .env.example (not referenced in code) |
| CI-only: GH_TOKEN | ℹ️ CI-ONLY | Not in .env.example — document if needed for local dev |
**UNDOCUMENTED keys (missing from .env.example):** 1
**STALE keys (in .env.example but unused):** 1
---
### B. Runtime Version Matrix
| Source | Version | Status |
|--------|---------|--------|
| .nvmrc | 22.3.0 | ✅ |
| package.json engines | >=20 | ✅ compatible |
| CI matrix | 20, 22 | ⚠️ CI tests Node 20 but .nvmrc pins 22.3.0 — local dev and one CI matrix arm differ |
| Docker base | node:lts | ⚠️ Imprecise tag — pin to node:22-alpine for reproducibility |
**Drift detected:** CI tests against Node 20 while local dev uses Node 22.3.0.
---
### C. CI Environment Variable Coverage
| .env.example Key | In CI env/secrets | Status |
|------------------|-------------------|--------|
| DATABASE_URL | ✅ via ${{ secrets.DATABASE_URL }} | — |
| STRIPE_KEY | ❌ Not found in any workflow | ⚠️ CI GAP |
---
### D. Docker Lockfile Consistency
| Check | Status |
|-------|--------|
| Lockfile copied into image | ✅ |
| `npm ci` used (not `npm install`) | ❌ Uses `npm install` — lockfile ignored at build time |
---
### Summary
| Check | Status |
|-------|--------|
| A. Variable key drift | ⚠️ 1 undocumented, 1 stale |
| B. Runtime version drift | ⚠️ CI/local mismatch on Node version |
| C. CI env coverage | ⚠️ STRIPE_KEY missing from CI |
| D. Docker lockfile | ❌ npm install used instead of npm ci |
**Overall: DRIFT DETECTED — 4 issues found (1 blocking, 3 advisory)**
### Remediation
**[BLOCKING] D — Docker uses `npm install` instead of `npm ci`**
Change line N of Dockerfile: `RUN npm install` → `RUN npm ci`
This ensures the Docker build uses exactly the versions in package-lock.json.
**[ADVISORY] A — STRIPE_WEBHOOK_SECRET undocumented**
Add to .env.example: `STRIPE_WEBHOOK_SECRET=` (with a comment explaining where to get it)
**[ADVISORY] B — Pin Docker base image**
Change `FROM node:lts` → `FROM node:22-alpine` to match .nvmrc
**[ADVISORY] C — STRIPE_KEY missing from CI**
Add to your workflow env block or GitHub Secrets and reference it as `${{ secrets.STRIPE_KEY }}`Blocking vs. advisory:
npm install instead of npm ci / poetry install) — build reproducibility is brokenAfter the report:
"Would you like me to fix the blocking issue in the Dockerfile, add missing keys to .env.example, or update the CI workflow to pin the runtime version?"Apply fixes only with explicit confirmation per item.
.env.example, Dockerfile, or CI workflows unless the user confirms..env.example does not exist but .env is committed (a security risk), flag it immediately: ".env appears to be committed to the repository — this may contain real secrets. Check .gitignore and rotate any exposed values."ci-cd-pipeline (set up CI from scratch), release-readiness (run before shipping to catch drift at release time), security-hardening (if secrets exposure is detected).~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.