Releases: DvGils/notenv
v0.17.1
A patch release: the session-key cache on macOS and Windows could store an entry that read back
already expired.
Fixed
- The session-key cache on macOS (Keychain) and Windows (DPAPI) no longer expires an entry the
instant it is stored. Both recorded the expiry deadline at one-second resolution, so an entry
written just before a second boundary rounded its deadline down and could read back as already
expired: the firstGetafterStoremissed, forcing an unnecessary re-prompt (and a flaky cache
test). The deadline is now nanosecond-resolution, so a TTL lasts its full duration. Linux was
unaffected (the kernel keyring enforces the timeout natively). A cache entry written by an earlier
version is treated as expired and re-fetched once.
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.17.0
The ergonomics-and-offboarding release. The first run loses its rough edges, output masking gets
meaningfully stronger (it now scrubs a secret's common encodings, not just its literal bytes), and a
clean way out arrives: export your plaintext and delete a vault, without notenv ever writing a secret
to a file.
Added
notenv export: take your secrets and leave. Prints a namespace (or, with--all, the whole
vault) as.envto standard output, the inverse ofimport, sonotenv export | notenv import
round-trips a namespace. notenv never opens a plaintext file itself; it writes only to stdout, and
redirecting it (notenv export > .env) is your deliberate act, so the no-plaintext-on-disk promise
holds. There is deliberately no--outputflag. Bulk plaintext egress is gated likerun --no-mask: it asks for the vault's primary passphrase even when the session key is cached, refuses
without a terminal, and a machine identity cannot perform it.--jsonemits a structured form.notenv vault delete: remove a vault for good. Deletes a configured vault's objects, this
machine's trust state for it (the rollback pin and cached key), and its entry in the machine
config, behind the primary passphrase and a type-the-name confirmation. notenv removes the live
vault; a versioned remote's history and your own backups are the provider's to purge, and the
message says so. There is no--force: notenv only ever destroys a vault you can prove you own. If
you have lost the passphrase, delete the storage yourself and runnotenv key forget.
Changed
- Output masking now catches a secret's common encodings, not just its literal bytes. Because
notenv knows the exact values it injected, it scrubs each one along with its base64 (standard and
URL, padded and not), hex (upper and lower), and percent-encoded forms from captured output, with
none of the false-positive risk a guessing scanner carries. So a token base64'd into an auth header
or a password percent-encoded into a logged URL is caught now. It stays accident-proofing, not a
boundary: a value transformed in a way notenv does not anticipate, or embedded in a larger blob
before encoding, still passes, as do values shorter than 6 bytes. A first-byte index keeps matching
fast as the pattern set grows, so injecting many secrets stays snappy. - A smoother first run.
notenv initno longer prompts for a namespace: it defaults to the
directory name and shows it in the result (--namespacestill overrides). A first-timenotenv setupno longer asks "add another storage?" right after creating your first vault (re-running
setup still offers it). Andnotenv initconfirms before scaffolding a project in your home
directory or a filesystem root, so a mistypedcddoes not quietly make$HOMEa notenv project.
Documentation
- The masking limits are restated now that common encodings are caught: the example of what defeats
masking is a transform notenv does not anticipate (piping a value throughrev), notbase64,
which is now masked. The threat model, the AI-agents guide, and therunhelp text reflect this.
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.16.0
The pre-v1 hardening release. A five-way bug hunt across the storage, crypto, key-management,
backend, and CLI layers, run before the v1 freeze with every finding fixed; the first slice of
the credential broker; a way out of a corrupt object; and a crypto format that now describes its
own algorithms, so a future KDF or post-quantum recipient is an additive change instead of a
format break.
Security
- The vault credential no longer leaks into child environments. A recipient identity supplied
viaNOTENV_IDENTITYdecrypts the whole vault, andrun,mcp run_with_secrets, and the
editeditor all built the child's environment from the parent's unfiltered, so the
master-equivalent key was inherited by every child and could land in a log, a crash report, or
anenvdump.NOTENV_IDENTITYis now stripped from the child. This is the broker's first
slice: accident-proofing for the credential, the analog of the output masker, not a containment
wall. A same-uid child can still read the value deliberately (that is the OS boundary's job, and
the threat model says so), but the accidental leak is closed. A nestednotenv run -- notenv …
still works, because the inner notenv unlocks from the session cache rather than the variable. RemoteandBaseare validated before they reach rclone. They were taken raw and kept
safe only by the end-of-options marker at the exec boundary. They are now rejected on write for
a leading dash, control characters, and (for the base)..path segments, so a later refactor
that trusts the marker less cannot reopen flag injection.
Added
- A way out of a corrupt or missing object. A single recorded object that was missing,
undecryptable, or failed its manifest MAC used to fail every read and every compaction for the
whole namespace, with no tool-supported recovery. Nownotenv key evict-object <key>drops the
object from the manifest and storage (acknowledged data loss, gated behind--yes), and
notenv run|list --skip-corruptreads past untrustable objects non-destructively, resolving
every key it still can and naming each one it dropped. The default fold stays fail-closed:
salvage is opt-in, so a dropped or tampered object is never silently skipped.doctornow
decrypts and MAC-checks every recorded object when a session key is cached, so a
present-but-corrupt object is caught before a read trips over it, and its advice no longer points
at a compaction that cannot run. - The crypto format is now self-describing. The header records the cipher suite it uses and
each passphrase slot records its KDF, and a build refuses, fail-closed, any suite or KDF it does
not implement. notenv ships exactly one of each, so nothing changes today, but a future stronger
KDF or a hybrid post-quantum recipient becomes an additive registry entry rather than a format
break. notenv owns the choice entirely; there is no user-facing suite concept.
Fixed
- A reset or migrated sequence counter no longer causes a false replay lockout. Replay
detection assumed a machine's sequence counter only moves forward, but that counter is local,
per-scope state classified as silently migratable, while the high-water it is checked against
lives on the remote keyed by the durable machine id. Losing or migrating the counter (a restore
that omits it, or renaming a remote, which restarts it at zero) while the machine id survived
could make an honest write look like a replayed deletion and fail the namespace closed for every
machine. A write's sequence number is now floored at the fold's observed high-water for that
machine, so a lost counter can never reissue a number already on storage. - Concurrent
rotate-masterno longer strands teammates with a false rollback alarm. The
rotation history lived in a separate object with no compare-and-swap, so two machines rotating
from the same master could have the loser's write erase the winner's transition record, leaving a
third machine pinned at the old master unable to find a signed path forward. The history now rides
inside the header, so a master change and its signed record land in one compare-and-swap. Unlockno longer aborts on a slot that decrypts but does not open the master. A stale slot
from an interruptedkey rm, or one planted under a known passphrase, could shadow a valid later
slot sharing that passphrase. The loop now skips a slot whose key is not a recipient of the
master and continues, failing only once every slot is exhausted, with a distinct message when a
passphrase matched a slot that could not open the vault (a tampered or half-rotated header).restore-backupre-pins after restoring. Every other header writer advances the local pin;
this one did not, so restoring a one-revision-old backup after the pin had moved ahead raised a
rollback alarm on the operator's own recovery and pointed them at a security override. It now
re-pins to the restored header (gated on the cached master verifying it), so an honest restore no
longer reads as an attack.set-primaryonto a machine slot is refused. Assigning primary to a recipient
(machine/teammate) slot froze governance: transferring or removing primary then required that one
identity, and losing it made primary unrecoverable. Primary is a human governance anchor; a
machine identity can no longer hold it.- A Ctrl-C during a hidden prompt no longer leaves the terminal with echo off. Hidden prompts
now restore terminal state before exiting on a signal, so a subsequently typed secret is not
invisible. - The MCP output masker no longer skips short values. The CLI masker skips values under six
bytes to avoid shredding tokens like8080; on the MCP surface the masked stream feeds a model's
context, where a short PIN should not pass through, so the MCP masker has no floor. - Tidies: a leading byte-order mark on an imported
.envis stripped instead of becoming part of
the first key; the local backend's path guard rejects a..path component rather than any..
substring; object names now carry 64 bits of randomness.
Breaking changes
- Header format version 5. The rotation history moved into the header, and the header gained
the cipher-suite and per-slot KDF identifiers. v4 vaults (0.13 through 0.15) are not readable by
this build (pre-1.0, no migration path, consistent with earlier header bumps). The
segment/snapshot payload format is unchanged at version 3.
Documentation
- The threat model and the agents/CI guidance frame the broker's first slice: what the credential
strip contains (accidental leakage into child environments) and what it does not (a same-uid
process that is actively looking, or the orchestrator that holds the credential by necessity),
with the OS trust boundary that real containment requires.
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.15.2
Release 0.15.2. See https://github.com/DvGils/notenv/blob/v0.15.2/CHANGELOG.md
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.15.1
Release 0.15.1. See https://github.com/DvGils/notenv/blob/v0.15.1/CHANGELOG.md
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.15.0
Deletions become durable, the passphrase prompt stops being a tax on macOS and Windows,
and the agent surface ships in both formats agents come in.
Fixed
- A deleted key can no longer resurrect. Deletions were the only operation whose
evidence compaction destroyed: tombstones were dropped at fold time, so a write that was
in flight while the namespace compacted (a slow upload, a laptop suspended mid-set)
could bring a deleted key back, silently, even when the deletion was strictly newer, with
the outcome depending on whether a cleanup happened to run in the window. Snapshots now
retain tombstones with full provenance, so a deletion keeps winning exactly what the
ordering rule says it wins, compaction is value-transparent again, and a late write that
loses is reported as a conflict. The storage format version bumps to 3; 0.14 vaults are
not readable by this build (pre-1.0, no migration path). The 0.11 notes called the
previous payload change "deliberately the last before the freeze": that claim was wrong
and is withdrawn, not replaced. The simulation fuzzer's oracle is now asserted in the
compaction world too; three inputs already in its corpus trip the old behavior.
Added
- Session key caching on macOS (Keychain) and Windows (DPAPI). Unlock once per session
on every platform instead of once per command. The native stores hold the cached key as
ciphertext under your login credentials, the same custody class machine identities
already use; what is weaker than the Linux kernel keyring is stated in the
caching guide: the TTL is enforced
lazily on read, and an expired entry persists encrypted until its next touch. Set
crypto.cache_ttl = "0"to prompt every time. CI now runs the full test suite on macOS
and Windows, not just cross-builds. - The MCP server grows up and drops its experimental label. Four tools, none of which
accepts or returns a secret value, none of which writes to a vault:list_namespaces
(the discovery hop, no unlock needed),list_secrets,run_with_secrets, anddoctor
(the checkup findings as data). Results are typed (structuredContentwith declared
output schemas) and the read tools carryreadOnlyHint. A golden file pins the entire
tool surface. The headless recipe is documented: session-cached key orNOTENV_IDENTITY
to unlock,NOTENV_ACCEPT_NAMESPACEfor first use of existing namespaces. - An installable agent skill at
skills/notenv/SKILL.md: the CLI surface and the
never-see-values rules in the Agent Skills format shell-first agents understand. A skill
for agents with a shell, MCP for agents without one, the same surface either way.
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.14.0
The hardening and dailiness release. A security audit of 0.13 (no High or Medium findings)
and a sweep of the threat model's own caveats drive most of it: the gaps that were wrinkles
rather than physics get closed, and the daily loop gets its missing verb.
Added
notenv edit: bulk editing that never displays a value. The$EDITORbuffer shows
every existing value as<keep>: replace it to set, delete the line to unset, add lines
to create, edit the comment above a key to change its description. The diff lands as one
recorded write, new keys are declared in the contract, and a key that also changed on
another machine while the buffer was open stops the save with the key named. The buffer
never contains stored plaintext, so it can leak at most what you typed into it; it lives
in the RAM-backed runtime dir on Linux and is removed on exit and on signals.- The onboarding string now proves which vault it is for.
key addprints the one-time
passphrase with a short fingerprint of the vault appended; the invited teammate's first
contact verifies the served header against it before anything is trusted, so a
substituted vault is refused instead of silently pinned. A legitimate re-key between the
invite and first contact passes by proving itself through the signed rotation chain.
Trust-on-first-use is closed for onboarded teammates. notenv doctor. One read-only checkup for the known problem states: a vanished or
unreadable header, a pending rollback, a replaced vault, unfinished onboarding, objects a
crashed write left unrecorded, recorded objects that are missing. It recommends and never
fixes, never prompts, and exits 1 on findings so CI can run it.- Generated root passphrases.
setupaccepts Enter to generate a six-word passphrase,
printed once; typed passphrases under 12 characters draw a warning naming the offline
brute-force attack, at creation, atkey rotate, and during onboarding.
Changed
- Namespace confirmation fails closed without a terminal (audit finding). The first use
of a namespace that already holds secrets used to warn and proceed in CI and agent
harnesses; a malicious repository's committed contract on a shared runner could reach
another project's secrets that way. It now refuses unlessNOTENV_ACCEPT_NAMESPACEnames
the exact namespace; the value is a list of names rather than a yes-flag, because a
contract cannot write the runner's environment. Breaking for CI flows that relied on the
warn-and-proceed behavior: add the variable to the pipeline. run --no-maskasks for a freshly typed passphrase, even when the session key is
cached. Sending raw secret values to a captured stream is now a human's act: prompts read
the terminal device, so an agent holding a warm cache cannot complete one. Strict on
purpose: no identity satisfies it and no environment variable bypasses it.- rclone invocations carry an end-of-options marker (audit hardening). The argv builder
separates flags from paths itself, so the guarantee that no name is ever parsed as a flag
lives at the exec boundary instead of in upstream validation.
Documentation
- The threat model narrows its trust-on-first-use limitation (closed for onboarded
teammates), upgrades the malicious-contract property to cover headless runners, and
cross-referencesdoctorfrom the known limitations. The teams, new-machine, CI, agents,
and environment pages cover the onboarding string,NOTENV_ACCEPT_NAMESPACE, and the
unmask gate.
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.13.0
The principals release: passphrases are for people, identities are for machines. A vault
concentrates risk, so no file at rest may be key-equivalent; this release makes that a
structural property instead of advice. Teammates onboard with a one-time passphrase and end
up with a credential only they know; machines enroll with an identity that lives in the
platform's secret store; the on-disk identity file ceases to exist.
Added
- Teammate onboarding with a one-time passphrase.
notenv key add alicegenerates a
high-entropy onboarding passphrase (six wordlist words), prints it once, and marks the
slot provisional. Alice's first notenv command refuses to proceed until she replaces it
with a passphrase only she knows; the one-time passphrase stops working at that moment,
and the issuer no longer knows any credential of hers. An interceptor would need the
passphrase and storage read access during that window;key rotate-masteris the remedy
if you suspect one. - Machine enrollment.
notenv key add --machine cienrolls a CI job or agent: it
prints a new age identity exactly once, for the platform's secret store, and saves it
nowhere.--recipient age1...enrolls a public key the machine generated itself. Pair
withNOTENV_READONLY=1and a read-only storage credential where the machine only reads. key listspeaks principals. The table shows human (passphrase), human
(provisional), or machine (identity), plus when each slot was added, and warns about
provisional slots older than a week (the holder never finished onboarding). The--json
shape gainsprovisionalandadded, both omitted when unset.
Changed
- Header format v4. Slots carry the provisional flag and an advisory creation time.
Older builds refuse a v4 header loudly; this build does not read v3 vaults (pre-1.0,
no migration path, consistent with earlier format bumps). key addis name-first. The slot name is a positional argument; the--passphrase
and--nameflags are gone. Adding a backup passphrase slot for yourself is the same
flow as onboarding a teammate: replace the one-time passphrase on first use.NOTENV_IDENTITYis the only identity source. Inline value, or a path your platform
materialized. notenv no longer reads (or writes) any identity file of its own.
Removed
notenv key gen-identityand the default identity file. A plaintext age identity at
a well-known path was the one key-equivalent artifact notenv left at rest, exactly the
kind of path infostealers harvest. Humans never need one (passphrases plus the session
cache cover every interactive flow), and machines get theirs from a secret store. There
is no notenv-owned credential path left for a stealer list to name.
Documentation
- The threat model states the credential model. A new "Credentials at rest" section
sets the bar (no file at rest may be key-equivalent), scores every unlock path against
it, and names the honest residuals of concentrating secrets in a vault: the offline
brute-force surface against a passphrase slot, and the onboarding window. The teams, CI,
agents, and new-machine guides are rewritten around the split.
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.12.1
Release 0.12.1. See https://github.com/DvGils/notenv/blob/v0.12.1/CHANGELOG.md
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.
v0.12.0
The documentation release. notenv gets a proper documentation site, the README becomes a
landing page that points into it, and the user-facing output gets a polish pass. Nothing
about the storage format or command behavior changes.
Documentation
- A documentation site at https://dvgils.github.io/notenv, built with MkDocs
(Material) and published fromdocs/by a GitHub Pages workflow. It covers getting
started, task guides (teams and keys, cloud remotes, CI, AI agents, caching and
performance), a command and configuration reference, the concepts behind the design,
and the full threat model. - The README is now a landing page. It keeps the pitch, the comparison table, and a
quick start, and links into the site for everything deeper. - The threat model and security policy moved into the site.
THREAT_MODEL.mdis now
a pointer to the site's threat model;SECURITY.mdkeeps the private vulnerability
reporting link and points its scope there too.
Changed
- Clearer error for a vault in an unreadable older format. Two messages pointed at
notenv key migrate, removed back in 0.9. A vault written in a storage format this
build no longer reads now says exactly that, instead of naming a command that no longer
exists. - Consistent house style in CLI output. Removed em-dashes from messages, prompts, and
help text. Wording only; no flags, output shapes, or exit codes changed.
Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.