Repository navigation
v0.6.0
evnx v0.6.0
The first release since 0.5.0 that contains new code. 0.5.1 and 0.5.2 were
release engineering; ten pull requests have landed since.
Security — a notice carried forward
evnx validate --fix on v0.4.x or earlier should rotate the
secret it generated. The value was derived from roughly 25 bits of time
rather than randomness — 48 of its 64 hexadecimal characters were always 0 —
and the output announced it as "Generated secure secret", so there was nothing to
suggest it needed replacing. Where such a value ended up as a session-signing key
or a JWT secret, it is guessable by anyone who knows approximately when it was
generated.
evnx migrate --to github on a version before 0.5.0 should
rotate everything it uploaded, at the source. Values were base64-encoded rather
than encrypted: the function was named encrypt_for_github and the payload went
into a field called encrypted_value, so both the code and the wire format
described encryption that had not happened.
Both were fixed in 0.5.0. This notice is repeated here because it was written
into docs/releases/v0.5.0.md and nothing published that file — the release body
was generated from a template that read no file at all, so the notice went unread
across three releases. That is fixed in this release too; see Fixed below.
Affected packages, all < 0.5.0: crates.io/evnx, PyPI/evnx, npm/@evnx/cli.
See the repository's security advisories for ranges and remediation.
Why this is 0.6.0 and not 0.5.3
Two changes alter behaviour rather than only adding to it, and in 0.x semver
puts a compatibility change in the minor position:
- Escape sequences in a multiline double-quoted value are now interpreted,
matching what a single-line value always did. A multiline value containing a
backslash changes meaning. See Changed below. - A value recognised only by its shape, behind a build-time public prefix, is
raised fromlowtomedium. A--severity mediumgate that passed can now
fail. See Added below.
Neither is large, and no correct program depended on either — but both are the
kind of change someone should read a version number and expect.
Added
-
A secret behind a build-time public prefix is now called what it is.
scan
reported a Stripe key inVITE_STRIPE_SECRET_KEYcorrectly as high confidence,
then told you togit filter-repoand force push — work that contains nothing,
because the value is compiled into every bundle already served. Someone
following it literally would rewrite history and believe they were done.A finding whose variable carries
NEXT_PUBLIC_,VITE_,REACT_APP_,
EXPO_PUBLIC_,NUXT_PUBLIC_,GATSBY_,VUE_APP_,STORYBOOK_or
PUBLIC_now says which tool inlines it, names the prefix-stripped variable to
rename it to, and leads the next steps with renaming rather than history
rewriting. The prefix is in the JSON aspublic_prefix, so CI can tell
"leaked" from "published". (#78)Severity is raised where it was wrong: a known provider format behind a public
prefix is high, and a value recognised only by shape is raised to
medium — enough to be seen, not enough to fail a--severity highgate on
an analytics key that is meant to ship.⚠️ Escalation only ever raises a
finding that already exists, soPUBLIC_KEY=<a public key>and
NEXT_PUBLIC_API_URL=<a url>are still not findings at all. -
.ipynbfiles are scanned. Identical JSON was scanned as.jsonand
ignored as.ipynb— a filter, not a parser. Notebooks are where ML work keeps
keys, and a key often appears twice: once in asourcecell and again in an
outputscell where it was echoed. (#78) -
vault share,vault revokeandvault roleaccept both--withand
--user.sharetook--withwhile the other two took--userfor the
same thing, so whichever you learned first was wrong half the time. Neither is
deprecated. (#78) -
scripts/verify-claims.sh— documented claims about evnx are now checked by
running it. Each claim has a stable id and a verdict ofCONFIRMED,REFUTED
orSKIP, and the exit status mirrors evnx's own contract:0all confirmed,
1something refuted,2could not run. "No claims ran" is2, because a
verification script that exits 0 when it verified nothing is the bug it exists
to catch. (#74)
Fixed
-
The GitHub Release body now carries this changelog.
release.ymlgenerated
the body from an inline template — installation instructions for the seven
channels, and nothing about the release itself — so anything written about a
version never reached the page people land on. The rotation notice above lived
indocs/releases/v0.5.0.md, which no workflow reads, and went unread across
three releases as a result.The section between
## [VERSION]and the next heading is now lifted from
CHANGELOG.mdand placed above the installation instructions, so a reader
who needs to rotate a credential does not have to scroll past seven package
managers to learn it. A missing heading warns rather than failing the release,
and the body falls back to installation instructions alone. -
evnx scanno longer reports a clean result for a scan that examined nothing.
dist/,build/,node_modules/andtarget/are skipped on a directory
walk, and naming a file inside one was silently dropped — so a CI gate pointed
at a build artifact printed✓ No secrets detected,0 files scannedand exit
0, on a file holding a live Stripe key. The extension allowlist did the same
to anything it did not recognise:evnx scan key.pemscanned nothing and called
it clean.Two changes, the second being the general one: a path you name is always
scanned — the default exclusions and the allowlist exist to stop a directory
walk reading a million vendored files, and were never a veto over an argument —
and zero files scanned is never success, now exit2, because nothing was
found and nothing was looked at. (#71) -
The private-key detector could not fire outside a
.envfile.
-----BEGIN RSA PRIVATE KEY-----and its four siblings went undetected in every
source and.pemfile.extract_tokenssplits on whitespace and keeps only
tokens longer than 20 characters, and the header splits into-----BEGIN,
RSA,PRIVATE,KEY-----— every piece too short to survive. Undetectable in
exactly the files private keys live in.Fixed by making the built-in detectors rules on the same engine as custom
ones, matched against whole lines throughPatternSet. Custom
[[scan.patterns]]were never affected, so a rule a user wrote was strictly
stronger than one evnx shipped — that was the real defect. A dead
get_patterns()table of ~20 detectors, which nothing called while a
hand-writtenRegex::is_matchchain did the work, is gone with it. (#72) -
evnx migrate's missing-file hint named--env, a flag it does not have.
It is--source-file. (#70) -
The crate no longer ships environment files.
.env.backupand
.envx/placeholders.jsonwere committed in March 2026 and shipped to crates.io
in 0.5.0, 0.5.1 and 0.5.2, because this repository's own.gitignoreused the
narrow.env/.env.localconvention thatevnx inithad already moved away
from. The repo now uses the entriesevnx initwrites, and CI fails if
cargo package --listcontains either path. Published versions cannot be
altered; this stops it recurring. (#73) -
npm publishing downloaded every artifact in the release run, not just the
binaries.docker/build-push-actionuploads a build record of its own — a
.dockerbuildblob that is not a plain zip — and unzipping it killed the 0.5.2
npm publish withADM-ZIP: Invalid or unsupported zip format. Two earlier
failed tags had hidden it, because the docker job only produced one once the
release got that far. Filtered to^evnx-. (#69) -
npm publishing now reads the registry back instead of trusting the publish
step.npm publishreturning 0 means the tarball was accepted — not that the
version resolves, and not that thelatestdist-tag moved. The smoke test also
gave up after 90 seconds, so 0.5.2 published correctly to all six packages and
went red anyway.The registry is now polled with a 300-second budget and the install runs once,
after it is known to be there. Three things are newly checked: thatlatest
actually points at the new version (the test installed a pinned version, so a
stale tag was invisible), that all five platform packages resolve — they are
optionalDependencies, which npm skips without failing, so a missing one gave a
clean install and a binary absent at runtime on that architecture — and that
evnx --versionprints what was published. A failed upstream release is also a
failure here rather than a silent skip, which used to leave a green check beside
a red release. (#75) -
An escaped
\"no longer ends a multiline value.Parser::is_closed_quote
testedends_with('"'), so forB="line1 \"q\"the final character was a
quote — the trailing one of an escaped\"— and the parser concluded the value
had closed, reportingInvalid format at line 2: missing '=' separatoron the
continuation line. The check now requires the run of backslashes before the
closing quote to be of even length. Single-quoted and backtick values are
literal and were never affected.The same bug had a second site: the continuation branch used
strip_suffix(ml_quote), so a value could survive its opening line and then be
cut short by a continuation line ending in\". Both now go through one shared
function, along withevnx doctor— see below. (#77) -
evnx doctorno longer reports the continuation lines of a multiline value as
invalid syntax. It checked.envsyntax line by line, with no notion of a
value spanning lines, so a PEM key produced two or three complaints about a file
thatvalidate,convertandscanall accepted. It now tracks the open quote
using the parser's own helpers, so the two cannot disagree about where a value
ends.doctoralso now reports a quoted value whose quote never closes. The
line-based check could not represent that at all, so it called such a file clean
whilevalidatefailed on it — the same disagreement in the opposite direction. (#77)
Changed
-
⚠️ Escape sequences in a multiline double-quoted value are now interpreted,
matching the single-line behaviour. The accumulated value was inserted raw, so
"x\ty"produced a tab on one line but a literal backslash andtacross two —
the same quote character with different escape rules depending on the line count,
which also made\"inexpressible in a multiline value at all.This changes existing values. A multiline double-quoted value containing a
backslash now resolves it. PEM keys, certificates and base64 blobs contain no
backslashes and are unaffected; a multiline JSON value with\nor\"inside
it is not. Use single quotes for literal content. (#77)
Documentation
-
The README said multiline and array values "will not parse correctly". Two
of its three examples parse fine, and multiline values have worked all along —
the claim came from a verification step that read an exit code without reading
the message: it wrote a two-line value, ranevnx validate --env .env.ml, got
exit2and recorded a parse error. The2was.env.example does not exist;
validate never parsed the file.The claim had spread into the planning material, where it became a roadmap item
proposing "parser support for quoted multi-line values" — work to build a
feature that already existed. It also meant the README contradicted the docs
site, whereconcepts-migrate-intro.mdxcorrectly said multi-line values are
handled. Rewritten from what the binary actually does. (#76)
Installation
Linux / macOS (shell script)
curl -sSL https://raw.githubusercontent.com/urwithajit9/evnx/main/scripts/install.sh | bashHomebrew (macOS and Linux)
brew install urwithajit9/evnx/evnxScoop (Windows, user-local)
scoop bucket add evnx https://github.com/urwithajit9/scoop-evnx
scoop install evnxWinget (Windows, system-wide)
winget install urwithajit9.evnxnpm / Node.js
npm install -g @evnx/cliPython (pipx)
pipx install evnxCargo
cargo install evnx --all-features
cargo installcompiles from source withdefault = [], so plain
cargo install evnxgives you a binary without theauth,
vaultandcloudcommands. Every other channel above ships them
already — the binaries are built with--all-features.
Checksums
SHA256 checksums are provided for each binary. Verify after download:
sha256sum -c evnx-*.tar.gz.sha256What's Changed
- fix(ci): npm publish downloads only the binary artifacts by @urwithajit9 in #69
- fix(migrate): the missing-file hint named a flag migrate does not have by @urwithajit9 in #70
- fix(scan): a scan that examined nothing is not a clean scan by @urwithajit9 in #71
- refactor(scan): built-in detectors are rules on the same engine as custom ones by @urwithajit9 in #72
- fix: stop shipping environment artifacts, and follow our own .gitignore advice by @urwithajit9 in #73
- test: add scripts/verify-claims.sh — assert on messages, not exit codes by @urwithajit9 in #74
- fix(ci): read the npm registry back instead of trusting the publish step by @urwithajit9 in #75
- docs: multiline values are supported — correct the record, and name what is not by @urwithajit9 in #76
- fix(parser): an escaped quote no longer ends a multiline value, and doctor agrees by @urwithajit9 in #77
- feat(scan): a secret behind a public prefix is already published, not merely leaked by @urwithajit9 in #78
- chore(release): 0.6.0 by @urwithajit9 in #79
- fix(ci): the release body carries the changelog, so a notice can actually reach it by @urwithajit9 in #80
Full Changelog: v0.5.2...v0.6.0
npm / Node.js
npm install -g @evnx/cli # global install
npx @evnx/cli --version # zero-install usage
npm install --save-dev @evnx/cli # project dev dependency