Summary
arkenv lint is a dev-time linter for .env* files on disk. It is complementary to arkenv check, not a second name for the same command.
check (#962, shipped in #1632): would this process boot? Subject is the resolved environment (process.env plus optional --env-file overlays) vs the schema. Key-level findings. CI / hooks.
lint (this issue): are these dotenv files well-authored? Subject is files on disk: syntax, duplicates, unquoted spaces, git-tracked secrets, example placeholders, and schema issues pinned to file:line:col. Editor problem matchers and pre-commit on .env*.
sync (#1234): writes files from the schema. Third sibling, same schema loader.
Do not fold lint into check --lint / --format unix. --env-file on check means “overlay into the resolved env,” not “lint these files.” Exit bands differ (check findings are exit 4; lint violations should fail a file-oriented run). CI must not pick up editor-only rules by accident.
Parser / coordinate-mapping strategy: ADR 0017.
How they build on each other
One pipeline, two subjects:
schema load (CLI schema loader)
↓
env record ──► validate() ──► EnvIssue[]
↑
dotenv parse
lint must reuse check’s schema loader and validate() — not fork a second validation path. It grows the dotenv parser so a parse yields both an env record and a coordinate map, runs static rules that only make sense on files, then remaps EnvIssue[] onto those coordinates.
Today’s parseDotenv already feeds check but drops coordinates and skips malformed lines. That is the stub lint replaces, not a second parser.
Status quo
arkenv check validates the resolved environment against the schema (--schema, repeatable --env-file, --json / --agent, exit 0 / 4 / 2). Diagnostics hang off the schema path. There is no command that treats .env* as source artifacts, no Unix path:line:col output, no cascade-by-mode, and no static dotenv rules.
Desired behavior
Command
arkenv lint [--schema <path>] [--env <name>]
Global flags (--quiet, --json, --agent, --help) apply as on other CLI commands.
Input
-
Discover the schema the same way check does (package.json arkenv entry, convention paths, --schema override).
-
Resolve dotenv files in standard cascade order for --env <name> (default process.env.NODE_ENV or development):
.env.[mode].local > .env.[mode] > .env.local > .env
Skip .env.local when mode === "test".
Parser
Coordinate-aware line scanner (ADR 0017): each defined key maps to file, line, and col. Malformed lines are diagnostics, not silent skips.
Validation
Run the same schema validate() as check against the cascaded record. Map each EnvIssue back to the winning file/line for that key (or a sensible “missing” location when the key is absent).
Output
- Default: Unix problem-matcher format
path/to/file.env:line:col: message
--json / --agent: settlement envelope; diagnostics include where: { path, line, column } pointing at dotenv files, not the schema module
Exit codes
0: no syntax, static, or schema violations
- Non-zero on any lint or validation finding (file-oriented; do not reuse
check’s exit 4 “completed with findings” band unless a later protocol ADR says otherwise)
Lint rules
- Rule 1 (Schema validation): values vs the imported schema (types, missing keys, bounds, patterns), mapped to coordinates.
- Rule 2 (Syntax): malformed lines (e.g. missing
=).
- Rule 3 (Unquoted spaces): warn on unquoted values that contain spaces.
- Rule 4 (Git): warn if sensitive
.env files are tracked instead of ignored.
- Rule 5 (Duplicates): error if a key is defined more than once in the same file.
- Rule 6 (Example placeholders):
.env.example must not contain real secrets.
Sub-tasks
Phase 1: Coordinate-aware parser
Phase 2: Command and cascade
Phase 3: Safe validation + coordinate mapping
Phase 4: Static rules
Phase 5: Formatting and exit
Phase 6: Tests
Out of scope
- Auto-fixing
.env files
- SARIF
- Changing
arkenv check’s contract (resolved env, --env-file as overlay, exit 4 on findings)
- Wrapping
dotenv-linter (rejected in ADR 0017)
- Folding this into flags on
check
Summary
arkenv lintis a dev-time linter for.env*files on disk. It is complementary toarkenv check, not a second name for the same command.check(#962, shipped in #1632): would this process boot? Subject is the resolved environment (process.envplus optional--env-fileoverlays) vs the schema. Key-level findings. CI / hooks.lint(this issue): are these dotenv files well-authored? Subject is files on disk: syntax, duplicates, unquoted spaces, git-tracked secrets, example placeholders, and schema issues pinned tofile:line:col. Editor problem matchers and pre-commit on.env*.sync(#1234): writes files from the schema. Third sibling, same schema loader.Do not fold lint into
check --lint/--format unix.--env-fileoncheckmeans “overlay into the resolved env,” not “lint these files.” Exit bands differ (checkfindings are exit4; lint violations should fail a file-oriented run). CI must not pick up editor-only rules by accident.Parser / coordinate-mapping strategy: ADR 0017.
How they build on each other
One pipeline, two subjects:
lintmust reusecheck’s schema loader andvalidate()— not fork a second validation path. It grows the dotenv parser so a parse yields both an env record and a coordinate map, runs static rules that only make sense on files, then remapsEnvIssue[]onto those coordinates.Today’s
parseDotenvalready feedscheckbut drops coordinates and skips malformed lines. That is the stub lint replaces, not a second parser.Status quo
arkenv checkvalidates the resolved environment against the schema (--schema, repeatable--env-file,--json/--agent, exit0/4/2). Diagnostics hang off the schema path. There is no command that treats.env*as source artifacts, no Unixpath:line:coloutput, no cascade-by-mode, and no static dotenv rules.Desired behavior
Command
Global flags (
--quiet,--json,--agent,--help) apply as on other CLI commands.Input
Discover the schema the same way
checkdoes (package.jsonarkenventry, convention paths,--schemaoverride).Resolve dotenv files in standard cascade order for
--env <name>(defaultprocess.env.NODE_ENVordevelopment):.env.[mode].local>.env.[mode]>.env.local>.envSkip
.env.localwhenmode === "test".Parser
Coordinate-aware line scanner (ADR 0017): each defined key maps to
file,line, andcol. Malformed lines are diagnostics, not silent skips.Validation
Run the same schema
validate()ascheckagainst the cascaded record. Map eachEnvIssueback to the winning file/line for that key (or a sensible “missing” location when the key is absent).Output
path/to/file.env:line:col: message--json/--agent: settlement envelope; diagnostics includewhere: { path, line, column }pointing at dotenv files, not the schema moduleExit codes
0: no syntax, static, or schema violationscheck’s exit4“completed with findings” band unless a later protocol ADR says otherwise)Lint rules
=)..envfiles are tracked instead of ignored..env.examplemust not contain real secrets.Sub-tasks
Phase 1: Coordinate-aware parser
file/line/col(and reports syntax instead of skipping)Phase 2: Command and cascade
arkenv lint(--schema,--env); do not overloadcheck --env-file.env.localskip in test mode)Phase 3: Safe validation + coordinate mapping
validate()on the cascaded record (same path ascheck)EnvIssue[]onto dotenv coordinatesPhase 4: Static rules
Phase 5: Formatting and exit
path:line:coldefaultwherePhase 6: Tests
arkenv lintarkenv checkbehavior is unchanged (resolved env,--env-fileoverlays, exit4on schema findings)Out of scope
.envfilesarkenv check’s contract (resolved env,--env-fileas overlay, exit4on findings)dotenv-linter(rejected in ADR 0017)check