release log · what shipped, when, why
Changelog
What shipped, when, and why. Every release of keynv — the AI-safe secrets vault — is logged here.
All notable changes to keynv will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[!NOTE] keynv is in pre-1.0. Minor versions (
0.x.0) may include breaking schema or API changes; patch versions (0.x.y) are backwards-compatible. The full stability promise lands at1.0.0. SeeCONTRIBUTING.md.
Unreleased
in progressAdded
- Text-surface protection — the first four primitives in keynv's runtime text-surface protection layer for AI workflows:
keynv doctor(read-only retro scan that counts likely-leaked secrets across shell histories, Claude Code session transcripts, and Cursor logs without ever echoing raw values),keynv scrub(atomic in-place rewrite with.keynv.bak.<ts>backups, JSONL-safe via the redactor's JSON-safe replacement tokens, with mtime-based active-write protection so a live Claude Code session isn't clobbered),keynv shell install|uninstall|status(a preventive zsh / bash / fish history hook that scrubs secret-shaped substrings before they land in the history file — pure POSIX-ERE regex, no per-command subprocess unless a match fires), andkeynv watch start|stop|status(a foreground real-time watcher daemon that subscribes to Claude Code transcript JSONL + Cursor log writes via chokidar, scrubs matched substrings on each change with a 1-second debounce, and persists lifecycle + scrub events to a local audit log at~/.local/share/keynv/watcher.log). Together these turn "your AI agent transcripts and shell history are silently leaking" into "you can see it, you can fix it, and you can stop it happening again — in real time." @keynv/text-surfaces— new workspace package exposing a stableTextSurfaceinterface (isPresent,enumerate,scan,rewrite), built-in surfaces for zsh / bash / fish history, Claude Code session JSONL, and Cursor logs, plus a genericscanFile/rewriteFilehelper for embedding the same primitive elsewhere. Scan results are non-sensitive by construction: previews are bounded to 3 chars and raw matched values never escape the package boundary.- Doctor entropy-detector path-prefix suppression — surface scans pass
/,./,../,~/, andhttp(s)/file://to the redactor'sexcludePrefixesso long filesystem paths and URLs stop tripping the high-entropy detector. Order-of-magnitude reduction in false positives on real machines without losing detection of vendor-prefixed tokens (AWS / GCP / GitHub / Stripe / Anthropic / OpenAI / Slack), JWTs, or credential-bearing URIs. - Shell hook pattern-bank mirror —
apps/cli/src/shell/templates.tsships a hand-mirrored POSIX-ERE subset of@keynv/redactor'sBUILTIN_PATTERNSso the preventive hook catches the same vendor-prefixed tokens, JWTs, and credential-bearing URIs as the redactor's batch API. Substitution uses#as the sed delimiter (not|, which collides with regex alternation in BSD sed on macOS). Re-runningkeynv shell installupgrades the hook body so a keynv upgrade ships any new pattern automatically. - Watcher daemon lifecycle — pidfile + status snapshot at
~/.local/share/keynv/watcher.{pid,status}, append-only JSONL audit at~/.local/share/keynv/watcher.log.keynv watch stopescalates SIGTERM to SIGKILL after a 15s default timeout because chokidar's polling-mode teardown on big transcript trees can hang; orphaning the watcher is worse than skipping a few teardown steps. Watcher rewrites passincludeActive: trueandbackup: falsetorewriteFile— a 1Hz cadence with backups would clutter the surface, and live transcripts are explicitly the point. - Fingerprint registry from resolution events —
keynv execnow connects to the watcher daemon (via a tiny newline-JSON RPC over~/.local/share/keynv/watcher.sock, chmod 0600) right afterresolveAllAliasesreturns, and registers each resolved value. The watcher holds those values in an in-memoryFingerprintRegistry(sha256[:8] fingerprints + plaintext, RAM-only, never persisted) and passes them asliteralsto each subsequentrewriteFilecall. Net effect: secrets with formats the regex pattern bank doesn't catch (custom in-house tokens, opaque UUIDs, internal API keys with no vendor prefix) get scrubbed from transcripts as long as they ever flowed throughkeynv exec. Fire-and-(briefly)-wait client with a 200ms timeout — if no watcher is running,keynv execsilently moves on without blocking.
Changed
- README rewrite — kills "self-hosted secrets manager" framing in favour of "runtime text-surface protection for AI coding workflows." Hero leads with
keynv doctoroutput as the demo (62,311 leak signals on a real machine). Adds a "What keynv is not" section explicitly disclaiming Vault / Doppler / Infisical / 1Password / SSO / compliance positioning. Quickstart restructured into five sequential steps mirroring the Phase A primitives (find → scrub → prevent → watch → use aliases). Status table reflects Phase A primitives shipped. Cloud-tier messaging dropped from the OSS-facing pitch (kept only in the License section as a commercial-license boundary note). - Threat model —
docs/02-threat-model.mdgains a runtime-text-surface section up top: explicit in-scope / out-of-scope lists per the new category, plus honest documentation of the sub-second race window between "secret lands in transcript" and "watcher rewrites it." The previous STRIDE walk-through is preserved below as the server-side audit reference. Trust is the product; overclaiming destroys it. - New docs —
docs/00-vision.mdis the long-form manifesto: the AI-era threat shift, the two-primitives thesis, what's explicitly not on the roadmap (sandboxing, enterprise RBAC, multi-region HA), and how to judge whether a feature belongs in keynv.docs/03-text-surfaces.mddefines theTextSurfaceinterface, the built-in surfaces, atomic-rewrite semantics, JSONL safety invariants, the race-window detail, and the false-positive / false-negative posture. - Redactor output rebuild is now O(n) (AUDIT-FINDINGS-4 Y1) —
redact()builds its output in a single left-to-right pass andjoins once, replacing the per-match right-to-leftslice + concatthat was O(matches × filesize) and dominated runtime on large scans (the "62,311 secrets" case). Output is byte-identical; only the allocation pattern changed.
Security
- Dev-dependency advisory bumps — pnpm overrides pin
form-data >=4.0.6(GHSA-hmw2-7cc7-3qxx, CRLF injection; reached only via thesupertesttest client) andvite >=8.0.16(GHSA-fx2h-pf6j-xcff,server.fs.denybypass; the test/build bundler). Both are dev-only and absent from the shipped runtime; bumped to keep the release security-gate (pnpm audit) clean. - Redaction tail-leak on overlapping matches (AUDIT-FINDINGS-4 K1) — the redactor's de-overlap step now extends the surviving match to the union span instead of dropping a later overlapping hit. Previously, when a shorter match started before and overlapped a longer one (reachably: two overlapping resolved alias values, e.g.
abc123def+def456ghiinabc123def456ghi), the longer match was discarded and its tail survived in cleartext. Sinceredact()is the single choke point behinddoctor,scrub,execoutput, and thewatchdaemon, this closed a leak on every surface. Regression test added with deliberately staggered overlapping literals. - MCP reference token burned before fetch (AUDIT-FINDINGS-4 K4) —
resolveTokenToValuenow peeks the single-use reference token, fetches the value, and only then consumes it. A transient network failure (or a project rename between issuance and resolution) previously burned the token before the fetch, permanently invalidating a valid capability token so the agent could not retry. Consumption is now gated on a successful fetch; the "never surface the raw value in an error" guarantee is preserved. keynv init --yesplaintext classification leak (AUDIT-FINDINGS-4 K2) — non-interactive auto-scan now treatsambiguousclassifier verdicts as secrets (fail-safe), routing them into the vault as alias references. Previously onlysecretverdicts were protected andambiguous/literalfell through into.keynv.env(advertised as safe-to-commit), so a single classifier false negative could write a real secret as committable plaintext with no human gate.- Web open-redirect in token refresh (AUDIT-FINDINGS-4 B1) —
GET /api/auth/refreshnow routes itsnexttarget throughsafeNext(), closing the same protocol-relative (//evil.com) / backslash (/\evil.com) open-redirect that AUDIT-FINDINGS-2 H1 fixed for login/register but left unpatched on this route. - Web open-redirect (AUDIT-FINDINGS-2 H1) — login and register actions now route the
nextredirect target throughsafeNext(), which rejects protocol-relative authorities (//evil.com), backslash variants (/\evil.com), header-injection control characters, and any value whose origin doesn't match the current host. - Web CSRF gap (H2) —
dismissOnboardingActionnow requires a CSRF token via the existingCsrfProvidercontext; silent reject on missing/forged tokens keeps the onboarding checklist visible under attack. - Server cross-org disclosure (H3) — non-admin
GET /v1/projectspath now scopes the database query byorg_idinstead of pulling every org's project rows into Node memory before an in-memory membership filter. - Server approvals race (H4) —
ensurePendingApprovalnow usesINSERT ... ON CONFLICT DO NOTHINGagainst a partial UNIQUE index on(project_id, alias, requester_user_id) WHERE status='pending', so concurrent reads of the same require-approval secret collapse to a single pending row instead of doubling the lead's queue. New migration0008_approvals_unique_pending.sqldedupes any pre-existing duplicates. - Server user-mutation TOCTOU (H5) —
PATCH /v1/users/:id/org-roleandDELETE /v1/users/:idnow includeorg_idalongsideidin theirWHEREclauses, closing the seam future concurrent org-move code paths would have tripped on. - Web CSP tightening (M1) — production
script-srcno longer carries'unsafe-eval'; dev keeps it for React Refresh.'unsafe-inline'for scripts is tracked as a follow-up (needs a per-request nonce middleware). - Web session sunset + HSTS (M2) — legacy v1 (HMAC-only) session cookies are accepted until
2026-07-01T00:00:00Zand rejected after, forcing a one-time re-login onto AES-256-GCM v2 cookies. Production responses now emitStrict-Transport-Security: max-age=31536000; includeSubDomains. - CLI init TUI secret preview (M3) —
keynv init's multiselect checklist no longer renders the first 27 characters of detected secrets. NewmaskedPreview(value, hint)shows[••••] (hint, N chars, fp:XXXX)where the four-hex fingerprint is the first four chars of SHA-256, enough for visual deduplication but cryptographically infeasible to invert.
Fixed
- Audit schema drift (H6) —
POST /v1/users(admin invite) was returning 500 because the audit payload includedorg_idbut the strict'user.invited'schema in@keynv/coredidn't accept it. Addingorg_idto the schema unblocks invites; the user row was already being inserted before the audit append failed, so the previous behaviour left split-brain state in the audit chain.
Tests
- New e2e specs at
tests/e2e/tests/csrf.spec.ts(verifies the register form rejects submission with the CSRF input removed) andtests/e2e/tests/security-headers.spec.ts(verifies CSP, X-Frame-Options, nosniff, Referrer-Policy, Permissions-Policy, and the production HSTS header). - New server regression tests: parallel
ensurePendingApprovalcalls collapse to one row; developer in org A cannot see org B's projects viaGET /v1/projects; owner B cannot patch or delete a user belonging to org A. - New unit tests:
safeNext(9 cases),maskedPreview(5 cases),securityHeaders(5 cases),decodeSessionv1 sunset (3 cases),dismissOnboardingActionCSRF gating (3 cases),'user.invited'audit schema happy path. - New regression tests for the AUDIT-FINDINGS-4 critical fixes: redactor no longer leaks the tail of a longer secret on partial overlap (staggered literals);
resolveTokenToValuekeeps the reference token redeemable after a transient fetch failure and remains single-use on success;keynv init --yesroutes anambiguousentry into the vault instead of writing it as plaintext.
- Text-surface protection — the first four primitives in keynv's runtime text-surface protection layer for AI workflows:
Fixed
- CLI browser authorization redirects now preserve the
codequery parameter when unauthenticated users are sent through login. - CLI browser auth start now uses the existing per-IP auth rate limit to reduce unauthenticated flow spam.
- CLI browser authorization redirects now preserve the
Added
- TUI-first onboarding: running
keynvnow guides first-time users through keynv.dev or self-hosted connection, then offers to set up the current project from the same menu. Users no longer need to discoverkeynv loginorkeynv initfor the happy path. - Release smoke checks: the release workflow now verifies the built CLI reports the tag version before publishing, then verifies the freshly published npm package with
npm execbefore completing. - Web CSRF protection: mutating web forms now include signed CSRF tokens, and server actions reject missing, expired, or tampered tokens with a safe error.
- Web session sealing: web session cookies now use AES-256-GCM sealing with legacy signed-cookie fallback, so refresh tokens are no longer stored as readable JSON in the browser cookie.
- Root error boundary: added a global public error boundary for root/layout-level failures.
Changed
- Documentation now presents
keynvas the primary command for connecting accounts and setting up projects; directlogin/initcommands remain available for automation and advanced use. - Secret deletion in the web UI now hides the deleted alias optimistically and restores it if the server action fails.
Fixed
- The token refresh route now decodes the session through the shared session helper instead of parsing raw cookie text, and no longer trusts a client-controlled
server_urlfallback. - CLI and docs no longer point users toward command-only setup when the guided TUI is available.
- Release publishing no longer masks npm publish failures with
|| true, preventing stale npm artifacts from looking successful.
- TUI-first onboarding: running
Fixed
- Server integration tests: all 4 route handlers that used
db.transaction(async (tx) => …)now use synchronous callbacks. better-sqlite3 v11 rejects Promise-returning transaction callbacks, causing a ROLLBACK and subsequent FK constraint failures on every insert-after-rollback. Affected routes:POST /v1/auth/register,POST /v1/projects,POST /v1/projects/:id/secrets/:env/:key/rotate,POST /v1/org. - E2E tests on Windows:
playwright.config.tsandnext.config.tsusednew URL(...).pathnamewhich produces/C:/...on Windows — an invalid path forspawncwd. Replaced withfileURLToPath()fromnode:url. - Redactor preview leak (M1):
preview()truncated to 4 chars for secrets > 4 chars long, allowing reconstruction of 5–8 char secrets. Tightened to 3 chars +.... Trust boundary documented inMatch.previewJSDoc. - X-Keynv-Agent trust boundary (M2): documented that the header is client-controlled and informational only — never used for authorization. Authenticated identity comes from JWT.
keynv initnow auto-generates.keynv.<env>.envfiles for each non-default environment (e.g..keynv.prod.env) sokeynv exec --from .keynv.prod.envworks immediately after init without manual file creation.
Security
- All 14/14 audit findings now resolved (M1, M2 were the last two deferrals).
- Server integration tests: all 4 route handlers that used
Added
keynv init --yesflag — auto-scans .env files, classifies entries, creates project, uploads secrets, and writes.keynv.envwithout any prompts. Enables fully automated CI/CD migration.resolveProjectIdnow performs case-insensitive project name matching.keynv execsubprocess PATH now includes nearestnode_modules/.binso project-local tools (next,vite, etc.) work withoutnpx.- Public registration endpoint
POST /v1/auth/register— opt-in viaKEYNV_PUBLIC_REGISTRATION=true. Creates a fresh org + owner user atomically and returns a JWT pair. Tighter per-IP rate limit (KEYNV_REGISTER_RATE_LIMIT_PER_MINUTE, default 5/min) lives on this route alone; authed routes keep the per-user budget. Self-host deployments default to off, so the open-source binary never grows multi-tenant signup unless the operator chooses it. This prepares a future hosted keynv Cloud preview without making it a shipped Cloud feature. - Web
/registerpage mirrors the login flow. Falls back to the login page with aregistration_disabledreason banner when the current instance has the flag off. /v1/healthexposes acapabilities.public_registrationboolean so the frontend can hide the signup link on instances that opted out.- Audit chain learned an
auth.registerevent type (validated payload schema in@keynv/core).
Fixed
- CLI no longer stays stuck in the interactive menu loop after completing login + init — auto-exits with a clean "All set." message.
keynv init --dry-runno longer requires an interactive terminal; works in CI with--env-fileand--secretflags.keynv secret listnow correctly extracts project names from@project.env.keyaliases viaparseAlias()instead of naive string splitting.- Secret key format preserved — env var names like
DATABASE_URLorOPENAI_API_KEYkeep their original case and underscores instead of being lowercased to kebab-case. - Server VERSION now reads from
package.jsoninstead of a hardcoded string that went out of sync. - Onboarding checklist in web dashboard uses a placeholder server URL instead of hardcoded
https://api.keynv.dev. keynv project describe <name>now accepts project name (in addition to ID). Previously only accepted ID.keynv initgains--env-file,--project,--env,--secretflags for non-interactive/CI usage.- CLI authorize page and project-switcher filter now use the shared
<Input />component for visual consistency. - Onboarding step 4 ("Onboard your AI agents") now marks complete once the user has resolved a secret via
keynv exec, instead of always showing as incomplete. keynv execwarns when no.keynv.envis found and suggests runningkeynv init.- Network errors in CLI commands now include the server URL and a
curl /v1/healthhint. - Keychain load failures are surfaced with an explicit error message instead of a silent "not logged in".
- Onboarding checklist dismissal is now persisted server-side (
users.onboarding_dismissed_at) so it persists across devices and browsers. DB migration:0005_onboarding_dismissed.sql.
Notes
- For the future hosted public beta, no usage limits are enforced yet. Free-tier quotas
(3 projects · 3 envs · 5 members) and paid plans live in the
closed-source
packages/ee/billing/path that ships in Phase 6. Early users will be grandfathered onto a generous plan when paid tiers activate; the OSS code path keepsplan='unlimited'for every org.
First public release candidate. Self-host stack is functional end-to-end: server, CLI, MCP server, web dashboard, and the AI-safety layer (privileged subprocess wrapper + output redactor) all ship together.
Added
Core vault & CLI (Phase 1)
- Hono-based REST server (
apps/server) with envelope encryption — per-project DEK, master KEK held in OS keychain ormaster.keyfile, XSalsa20-Poly1305 via libsodium. - Drizzle ORM over better-sqlite3 (WAL mode); hand-written migrations 0001–0003.
- 5-role RBAC (
packages/rbac):owner,admin,developer,reader,bot. Project-scoped membership overrides org-level role. - Append-only, SHA-256 hash-chained audit log;
POST /v1/audit/verifywalks the chain in 1000-row pages and threads the previous tail-hash across boundaries. - JWT access tokens (15-min) + opaque refresh tokens (sha256-hashed at rest, rotated).
- CLI (
apps/cli, Bun-compiled):login,project,secret,member,audit,whoami,exec --,redact,redact-stream,test,install.
AI-safety layer (Phase 2)
keynv exec --privileged subprocess wrapper. Resolves@project.env.keyaliases in a child process whose env/argv/stdin the agent's process tree never inherits.keynv-mcpMCP server (stdio transport).use_secret(alias)returns a single-use, 60s reference token;keynv exec --resolve NAME=<token>redeems it over a local 0600 socket and injects the value into the subprocess — the resolved value never crosses the MCP boundary back to the agent.- Output redactor (
packages/redactor): pattern bank (50+ vendor regex rules) + Shannon-entropy fallback. Streaming + batch APIs. - Per-agent onboarding via
keynv init: scans existing.envfiles, migrates secrets to vault, writes.keynv.envwith alias references only. Safe to commit.
Connection testers (Phase 3)
packages/testersadapter pattern. Built-in: Postgres, MySQL, Redis, SSH, HTTP (basic/bearer/custom header).keynv test @aliasreports OK/FAIL + latency, never values.
Web UI for team leads (Phase 4)
- Next.js 15 App Router dashboard (
apps/web), React 19, Tailwind 4, Radix primitives. - Linear/Raycast/Arc-style dark-first density. ⌘K command palette, g-prefix shortcuts.
- Pages:
/projects,/projects/[id]/{secrets,audit,members,status,approvals,settings},/audit,/admin/users,/settings/account,/login. - Mobile responsive (sheet drawer + hamburger top bar).
- Production-access approval state machine:
pending → granted | denied → expired, with grant/deny dialogs and expires_at-driven sweep. - Connection tester integration:
/projects/[id]/statusboard pulls live test results for every secret aliased as a connection target. - CLI tokens (
kt_prefix, sha256-hashed) for headless / CI auth.
Self-host deployment
- Multi-stage Dockerfile +
deploy/docker-compose.yml. - First-start auto-bootstrap: when
KEYNV_BOOTSTRAP_OWNER_*env vars are set and the vault is empty, the server creates the org + owner + initial project on boot. deploy/COOLIFY.mdwalkthrough for Coolify-based self-host.- Static landing page at
apps/landing/index.html.
Security
- Threat model (
docs/02-threat-model.md) — STRIDE walkthrough for every attack surface. - AUDIT-FINDINGS.md — original Phase 4 audit. All blockers (B1–B3) and Highs (H1, H4, H5) closed; deferred Mediums (H2, H3, M3–M6) closed.
- AUDIT-FINDINGS-PHASE5.md — public-release audit. Finding A1 (no rate limiting
on authenticated routes) resolved by
apps/server/src/lib/rate-limit.ts: per-user fixed-window-of-1-minute token bucket, default 120 req/min, configurable viaKEYNV_RATE_LIMIT_PER_MINUTE. Returns429 rate_limitedwithRetry-AfterandX-RateLimit-*headers. - Crypto: argon2id for password + refresh-token hashing; libsodium for vault
encryption;
crypto.timingSafeEqualon every token comparison. - gitleaks pre-commit hook + CI scan on full history.
- No secret values in logs — pino redactor pattern bank + per-route validation.
- No raw secret values from MCP — reference-token semantics enforced.
CI / tooling
ci.yml: lint + typecheck + test (Node 22) + gitleaks. Required formain.security.yml: nightlypnpm audit+ CodeQL.release.yml: tag-driven, drafts the GitHub Release with checksums. (Multi-arch Docker push + Bun-binary attach lands in0.1.0.)- biome (single tool — no ESLint, no Prettier).
- vitest + supertest (Node side);
bun:testfor the CLI binary.
License
- MIT — finalized; see
docs/decisions/0001-license-choice.md. - Phase 6 commercial modules will live under
packages/ee/*with a separate source-available license; nothing under that path today.
Known gaps (deferred to 0.1.x or 0.2.0)
- AF-1..5, AF-7 (Phase 5 audit sub-findings): real materialised tests for
tests/security/{env-files,env-enumeration,privileged-subprocess,mcp-reference-token}.test.ts, Argon2id parameters via env (KEYNV_ARGON2_*), JWT signing key rotation runbook indocs/01-architecture.md. Not release-blocking. - Signed binaries (cosign keyless OIDC) — deferred to
0.2.0. - Helm chart automated OCI push — chart stays in
deploy/helm/keynvbut no automated push for0.1.0. Re-add when k8s users ask. - Postgres adapter, KEK rotation flow, MFA, SSO/SAML, multi-region — Phase 6 (commercial tier + keynv Cloud).
- Hono-based REST server (