Skip to content

Releases: DvGils/notenv

v0.17.1

14 Jun 10:27
537d89b

Choose a tag to compare

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 first Get after Store missed, 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

14 Jun 10:08
1cd2d76

Choose a tag to compare

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 .env to standard output, the inverse of import, so notenv 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 --output flag. Bulk plaintext egress is gated like run --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. --json emits 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 run notenv 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 init no longer prompts for a namespace: it defaults to the
    directory name and shows it in the result (--namespace still overrides). A first-time notenv setup no longer asks "add another storage?" right after creating your first vault (re-running
    setup still offers it). And notenv init confirms before scaffolding a project in your home
    directory or a filesystem root, so a mistyped cd does not quietly make $HOME a 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 through rev), not base64,
    which is now masked. The threat model, the AI-agents guide, and the run help text reflect this.

Built reproducibly with GoReleaser. Artifacts are signed with cosign (keyless) and carry SLSA build provenance.

v0.16.0

13 Jun 22:42
756cb45

Choose a tag to compare

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
    via NOTENV_IDENTITY decrypts the whole vault, and run, mcp run_with_secrets, and the
    edit editor 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
    an env dump. NOTENV_IDENTITY is 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 nested notenv run -- notenv …
    still works, because the inner notenv unlocks from the session cache rather than the variable.
  • Remote and Base are 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. Now notenv key evict-object <key> drops the
    object from the manifest and storage (acknowledged data loss, gated behind --yes), and
    notenv run|list --skip-corrupt reads 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. doctor now
    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-master no 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.
  • Unlock no longer aborts on a slot that decrypts but does not open the master. A stale slot
    from an interrupted key 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-backup re-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-primary onto 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 like 8080; 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 .env is 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

13 Jun 13:34
a1103c8

Choose a tag to compare

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

13 Jun 00:34
8e74066

Choose a tag to compare

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

13 Jun 00:02
4c624ec

Choose a tag to compare

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, and doctor
    (the checkup findings as data). Results are typed (structuredContent with declared
    output schemas) and the read tools carry readOnlyHint. A golden file pins the entire
    tool surface. The headless recipe is documented: session-cached key or NOTENV_IDENTITY
    to unlock, NOTENV_ACCEPT_NAMESPACE for 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

12 Jun 22:13
dff9e44

Choose a tag to compare

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 $EDITOR buffer 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 add prints 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. setup accepts 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, at key 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 unless NOTENV_ACCEPT_NAMESPACE names
    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-mask asks 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-references doctor from 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

12 Jun 19:42
891c185

Choose a tag to compare

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 alice generates 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-master is the remedy
    if you suspect one.
  • Machine enrollment. notenv key add --machine ci enrolls 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
    with NOTENV_READONLY=1 and a read-only storage credential where the machine only reads.
  • key list speaks 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 gains provisional and added, 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 add is name-first. The slot name is a positional argument; the --passphrase
    and --name flags 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_IDENTITY is 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-identity and 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

12 Jun 17:22
9c2bead

Choose a tag to compare

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

12 Jun 17:16
e704e4e

Choose a tag to compare

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 from docs/ 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.md is now
    a pointer to the site's threat model; SECURITY.md keeps 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.