diff --git a/site/404.html b/site/404.html new file mode 100644 index 0000000..52ff1a1 --- /dev/null +++ b/site/404.html @@ -0,0 +1,26 @@ + + + + + + Page not found: envsitter + + + + + + + +
+
+

404

+

This page is redacted.

+

Nothing lives at this address. The value is not hidden, it just does not exist.

+ +
+
+ + diff --git a/site/DESIGN.md b/site/DESIGN.md new file mode 100644 index 0000000..d9cf4b6 --- /dev/null +++ b/site/DESIGN.md @@ -0,0 +1,110 @@ +# EnvSitter site design system + +Source of truth for `site/`. Every color, size, radius, shadow, and motion value in `assets/styles.css` maps to a token here. Add the token here first, then use it. This file is excluded from the published Pages artifact. + +## 0. Research Log + +- Embedded references: shortlisted `vercel`, `linear.app`, `supabase` (developer tooling, monochrome, precise). Picked `vercel` as Layer B for its achromatic palette, shadow-as-border elevation stack, compressed display tracking, and mono technical labels, which suit a CLI security library. Layer A: `minimalist-skill` for macro whitespace, plain copy, and no gradient or emoji slop. +- Lazyweb real-product screens: skipped. The site is a two-page static docs landing whose content is fixed by the READMEs; no product-screen grammar was needed. +- StyleGallery spatial patterns: skipped. Both pages are scroll-the-document pages with no app shell; the document owns the scroll. +- Imagen concept drafts: skipped. The focal object is a typographic artifact (redacted env file plus terminal), rendered in HTML/CSS so it stays exact, accessible, and theme-aware. + +## 1. Direction + +A redacted dossier for developer infrastructure. The canvas is engineering white (deep graphite in dark mode), and the only "imagery" is the act of redaction: secret values appear as solid redaction bars while keys stay legible in mono. The memorable moment is the hero, where values are redacted in front of the visitor and a terminal beside them answers with keys and booleans only. Color is functional: green for a match or an allowed action, red for a blocked one, blue for fingerprints and focus. + +## 2. Color tokens + +All theme tokens use `light-dark()` with `color-scheme: light dark`, so one declaration serves both themes and the OS preference decides. + +| Token | Light | Dark | Role | +|---|---|---|---| +| `--bg` | `#ffffff` | `#0a0a0a` | Page canvas | +| `--bg-subtle` | `#fafafa` | `#0f0f0f` | Alternate section band | +| `--surface` | `#ffffff` | `#141414` | Cards, panels | +| `--surface-2` | `#f5f5f5` | `#1c1c1c` | Inline code, chips, table header | +| `--text` | `#171717` | `#ededed` | Headings, primary text | +| `--text-2` | `#4d4d4d` | `#a3a3a3` | Body copy | +| `--text-3` | `#666666` | `#8f8f8f` | Captions, metadata (AA on both canvases) | +| `--ring` | `rgb(0 0 0 / 0.08)` | `rgb(255 255 255 / 0.10)` | Shadow-as-border | +| `--ring-strong` | `rgb(0 0 0 / 0.14)` | `rgb(255 255 255 / 0.18)` | Hover border, dividers | +| `--inner` | `#fafafa` | `rgb(255 255 255 / 0.03)` | Inner highlight ring in the card stack | +| `--redact` | `#171717` | `#e5e5e5` | Redaction bar ink | +| `--ok` | `#0f7b3d` | `#3fcf7f` | Match true, allowed | +| `--block` | `#c4291c` | `#ff7a6e` | Blocked, refused | +| `--hash` | `#0068d6` | `#5aa9ff` | Fingerprints, links | +| `--focus` | `hsl(212 100% 48%)` | same | Focus ring | + +Terminal palette (dark in both themes, so the terminal reads as a real console): `--term-bg #0b0b0c`, `--term-bar #151517`, `--term-text #e6e6e6`, `--term-muted #8f8f94`, `--term-ok #3fcf7f`, `--term-block #ff7a6e`, `--term-hash #5aa9ff`, `--term-key #f2c46d` (keys in JSON output). + +Atmosphere: hero dot grid `--dot` (`rgb(0 0 0 / 0.07)` / `rgb(255 255 255 / 0.06)`) masked to fade out, plus one radial glow `--glow` (`rgb(0 104 214 / 0.07)` / `rgb(90 169 255 / 0.08)`) behind the hero visual. + +## 3. Typography + +- `--font-sans`: `"Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif` (self-hosted variable woff2, OFL) +- `--font-mono`: `"Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace` (self-hosted variable woff2, OFL) +- Ligatures on (`font-feature-settings: "liga"`); weights 400 read, 500 interact, 600 announce. No 700. + +| Role | Size | Weight | Line height | Tracking | +|---|---|---|---|---| +| Display (h1) | `clamp(2.5rem, 4.2vw + 1rem, 4rem)` | 600 | 1.02 | `-0.045em` | +| Section (h2) | `clamp(1.75rem, 2vw + 1rem, 2.5rem)` | 600 | 1.1 | `-0.04em` | +| Card title (h3) | `1.25rem` | 600 | 1.3 | `-0.02em` | +| Lede | `clamp(1.0625rem, 0.4vw + 1rem, 1.25rem)` | 400 | 1.7 | normal | +| Body | `1rem` | 400 | 1.65 | normal | +| Small | `0.875rem` | 400/500 | 1.55 | normal | +| Eyebrow / label | `0.75rem` mono, uppercase | 500 | 1.3 | `0.08em` | +| Code | `0.8125rem` to `0.875rem` mono | 400 | 1.7 | normal | + +## 4. Space, radius, elevation + +- Spacing (4px base): `--s-1 4px`, `--s-2 8px`, `--s-3 12px`, `--s-4 16px`, `--s-5 24px`, `--s-6 32px`, `--s-7 48px`, `--s-8 64px`, `--s-9 96px`, `--s-10 128px`. Section padding `--section-y: clamp(64px, 9vw, 128px)`. +- Container: `--container: 1120px`, gutters `clamp(20px, 4vw, 32px)`. +- Radius: `--r-xs 4px` (inline code), `--r-sm 6px` (buttons), `--r-md 8px` (cards), `--r-lg 12px` (panels), `--r-pill 9999px` (badges only). +- Elevation (vercel shadow-as-border stack, never a CSS border on cards): + - `--e-1`: `0 0 0 1px var(--ring)` + - `--e-2`: `0 0 0 1px var(--ring), 0 2px 2px rgb(0 0 0 / 0.04)` + - `--e-3`: `0 0 0 1px var(--ring), 0 2px 2px rgb(0 0 0 / 0.04), 0 12px 24px -12px rgb(0 0 0 / 0.12), inset 0 0 0 1px var(--inner)` + - `--e-term`: `0 0 0 1px rgb(255 255 255 / 0.06), 0 24px 48px -24px rgb(0 0 0 / 0.45), 0 2px 6px rgb(0 0 0 / 0.10)` + +## 5. Primitives and states + +| Primitive | Anatomy | States | +|---|---|---| +| `.btn-primary` | `--text` fill, `--bg` text, `--r-sm`, 40px min height | hover: 85% fill; active: `scale(0.98)`; focus-visible: 2px `--focus` outline, 2px offset | +| `.btn-secondary` | `--surface` fill, `--e-1` ring | hover: `--ring-strong` ring; active, focus as above | +| `.copy-cmd` | mono command in a `--surface` box with `--e-2`, copy button on the right | button hidden until JS confirms clipboard support; copied: check glyph plus "Copied" announced through an `aria-live` region | +| `.card` | `--surface`, `--e-2`, `--r-md`, 24px to 32px padding | static (no hover lift: cards are not interactive) | +| `.card-link` | `.card` that is a link | hover: `--e-3`; focus-visible ring | +| `.badge` | pill, `--surface-2`, mono 12px uppercase | static | +| `.op` chip | mono op name plus one-line description | static | +| `.terminal` | window bar with three dots and a title, mono body lines `.ln-cmd`, `.ln-out`, `.ln-ok`, `.ln-block`, `.ln-muted` | static; horizontal scroll on narrow screens with a visible focusable region | +| `.envfile` | panel with key column and `.redact` bars | bars animate in once on load (see motion) | +| `.data-table` | hairline rows, `--surface-2` header | static | +| `.site-header` | sticky, translucent `--bg` with blur, hairline bottom | nav links: hover `--text`, current page `aria-current` | +| `.section-head` | eyebrow label plus h2 plus lede | static | + +No coloured edge stripes anywhere: state is shown with tone (`--surface-2`), ink color, or glyphs. + +## 6. Motion + +- Redaction reveal (signature): `.redact` bars `transform: scaleX(0 -> 1)` from the left, 520ms `cubic-bezier(0.16, 1, 0.3, 1)`, staggered 90ms per row, after a 250ms delay. It demonstrates the product idea (values become bars) and runs once. +- Buttons: `transform` and `background-color` 150ms ease; `scale(0.98)` on active. +- Copy confirmation: glyph swap with `opacity` 150ms. +- `prefers-reduced-motion: reduce`: no transitions or animations; bars render in their final state. +- Only `transform` and `opacity` animate. No scroll-triggered reveals. + +## 7. Responsive + +- Breakpoints: `640px` (nav collapses to essentials, single column), `960px` (hero switches from stacked to two columns, grids to 2 or 3 columns). +- Hero visual stacks below the copy under 960px. Terminal and code blocks scroll horizontally inside their own focusable region instead of overflowing the page. +- Tap targets at least 40px; body text never below 16px. + +## 8. Accessibility constraints and accepted debt + +- WCAG AA contrast for all text tokens in both themes (verified pairs listed in section 2); terminal colors checked against `--term-bg`. +- Semantic landmarks (`header`, `nav`, `main`, `footer`), one `h1` per page, skip link, `aria-current="page"` on the active nav item. +- Redaction bars are decorative: each value cell has `aria-label="value hidden"` text for assistive tech. +- Every page works without JavaScript; JS only adds copy buttons. +- Accepted debt: no manual theme toggle (the OS preference decides); no search; content is duplicated from the READMEs and must be updated by hand when the tool surface changes. +- Accepted debt: Lighthouse mobile performance is 99 (simulated slow 4G with gzip: FCP 1.2s, LCP 1.8s, TBT 0, CLS 0); desktop is 100, and accessibility, best practices, and SEO are 100 on both. The last point needs the stylesheet inlined at deploy time, deferred to keep the site build-free. Dropping the Geist preload was measured and made FCP worse (1.7s), so the preload stays. diff --git a/site/assets/favicon.svg b/site/assets/favicon.svg new file mode 100644 index 0000000..f101f87 --- /dev/null +++ b/site/assets/favicon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/site/assets/fonts/Geist-Variable.woff2 b/site/assets/fonts/Geist-Variable.woff2 new file mode 100644 index 0000000..543899b Binary files /dev/null and b/site/assets/fonts/Geist-Variable.woff2 differ diff --git a/site/assets/fonts/GeistMono-Variable.woff2 b/site/assets/fonts/GeistMono-Variable.woff2 new file mode 100644 index 0000000..1a44639 Binary files /dev/null and b/site/assets/fonts/GeistMono-Variable.woff2 differ diff --git a/site/assets/fonts/OFL.txt b/site/assets/fonts/OFL.txt new file mode 100644 index 0000000..8d003fe --- /dev/null +++ b/site/assets/fonts/OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2023 Vercel, in collaboration with basement.studio + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/assets/site.js b/site/assets/site.js new file mode 100644 index 0000000..f9992be --- /dev/null +++ b/site/assets/site.js @@ -0,0 +1,27 @@ +(() => { + if (!navigator.clipboard || !window.isSecureContext) return; + + const status = document.getElementById("copy-status"); + const announce = (message) => { + if (status) status.textContent = message; + }; + + document.querySelectorAll("button[data-copy]").forEach((button) => { + let resetTimer; + button.hidden = false; + button.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(button.dataset.copy); + button.dataset.state = "copied"; + announce("Copied to clipboard"); + clearTimeout(resetTimer); + resetTimer = setTimeout(() => { + delete button.dataset.state; + announce(""); + }, 2000); + } catch { + announce("Copy failed. Select the text and copy it manually."); + } + }); + }); +})(); diff --git a/site/assets/styles.css b/site/assets/styles.css new file mode 100644 index 0000000..e31dda7 --- /dev/null +++ b/site/assets/styles.css @@ -0,0 +1,1171 @@ +@font-face { + font-family: "Geist"; + src: url("fonts/Geist-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Geist Mono"; + src: url("fonts/GeistMono-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +:root { + color-scheme: light dark; + + --bg: light-dark(#ffffff, #0a0a0a); + --bg-subtle: light-dark(#fafafa, #0f0f0f); + --surface: light-dark(#ffffff, #141414); + --surface-2: light-dark(#f5f5f5, #1c1c1c); + --text: light-dark(#171717, #ededed); + --text-2: light-dark(#4d4d4d, #a3a3a3); + --text-3: light-dark(#666666, #8f8f8f); + --ring: light-dark(rgb(0 0 0 / 0.08), rgb(255 255 255 / 0.1)); + --ring-strong: light-dark(rgb(0 0 0 / 0.14), rgb(255 255 255 / 0.18)); + --inner: light-dark(#fafafa, rgb(255 255 255 / 0.03)); + --redact: light-dark(#171717, #e5e5e5); + --ok: light-dark(#0f7b3d, #3fcf7f); + --block: light-dark(#c4291c, #ff7a6e); + --hash: light-dark(#0068d6, #5aa9ff); + --focus: hsl(212 100% 48%); + --dot: light-dark(rgb(0 0 0 / 0.07), rgb(255 255 255 / 0.06)); + --glow: light-dark(rgb(0 104 214 / 0.07), rgb(90 169 255 / 0.08)); + + --term-bg: #0b0b0c; + --term-bar: #151517; + --term-text: #e6e6e6; + --term-muted: #8f8f94; + --term-ok: #3fcf7f; + --term-block: #ff7a6e; + --term-hash: #5aa9ff; + --term-key: #f2c46d; + + --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + + --s-1: 4px; + --s-2: 8px; + --s-3: 12px; + --s-4: 16px; + --s-5: 24px; + --s-6: 32px; + --s-7: 48px; + --s-8: 64px; + --s-9: 96px; + --s-10: 128px; + --section-y: clamp(64px, 9vw, 128px); + --container: 1120px; + --gutter: clamp(20px, 4vw, 32px); + + --r-xs: 4px; + --r-sm: 6px; + --r-md: 8px; + --r-lg: 12px; + --r-pill: 9999px; + + --e-1: 0 0 0 1px var(--ring); + --e-2: 0 0 0 1px var(--ring), 0 2px 2px rgb(0 0 0 / 0.04); + --e-3: 0 0 0 1px var(--ring), 0 2px 2px rgb(0 0 0 / 0.04), 0 12px 24px -12px rgb(0 0 0 / 0.12), inset 0 0 0 1px var(--inner); + --e-term: 0 0 0 1px rgb(255 255 255 / 0.06), 0 24px 48px -24px rgb(0 0 0 / 0.45), 0 2px 6px rgb(0 0 0 / 0.1); + + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +[hidden] { + display: none !important; +} + +html { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + scroll-padding-top: 80px; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.65; + font-feature-settings: "liga"; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +h1, +h2, +h3, +p, +ul, +ol, +figure, +pre { + margin: 0; +} + +a { + color: inherit; +} + +img, +svg { + display: block; +} + +code, +pre, +kbd { + font-family: var(--font-mono); + font-feature-settings: "liga"; +} + +:focus-visible { + outline: 2px solid var(--focus); + outline-offset: 2px; +} + +::selection { + background: light-dark(#cfe4ff, #1d3b5c); +} + +.skip-link { + position: absolute; + left: var(--s-4); + top: var(--s-3); + z-index: 100; + padding: var(--s-2) var(--s-4); + border-radius: var(--r-sm); + background: var(--text); + color: var(--bg); + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + transform: translateY(-200%); +} + +.skip-link:focus-visible { + transform: none; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +.container { + width: min(100% - 2 * var(--gutter), var(--container)); + margin-inline: auto; +} + +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: light-dark(rgb(255 255 255 / 0.82), rgb(10 10 10 / 0.78)); + backdrop-filter: saturate(180%) blur(12px); + -webkit-backdrop-filter: saturate(180%) blur(12px); + box-shadow: 0 1px 0 var(--ring); +} + +.header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--s-5); + min-height: 64px; +} + +.wordmark { + display: inline-flex; + align-items: center; + gap: var(--s-2); + min-height: 40px; + font-family: var(--font-mono); + font-size: 0.9375rem; + font-weight: 600; + letter-spacing: -0.02em; + text-decoration: none; +} + +.wordmark svg { + width: 22px; + height: 22px; +} + +.wordmark .wordmark-sub { + color: var(--text-3); + font-weight: 500; +} + +.site-nav { + display: flex; + align-items: center; + gap: var(--s-1); +} + +.site-nav a { + display: inline-flex; + align-items: center; + min-height: 40px; + padding: 0 var(--s-3); + border-radius: var(--r-sm); + color: var(--text-2); + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + white-space: nowrap; + transition: color 150ms ease, background-color 150ms ease; +} + +.site-nav a:hover, +.site-nav a[aria-current="page"] { + color: var(--text); +} + +.site-nav a[aria-current="page"] { + background: var(--surface-2); +} + +.site-nav .nav-github { + gap: var(--s-2); + color: var(--text); + box-shadow: var(--e-1); + margin-left: var(--s-2); +} + +.site-nav .nav-github svg { + width: 16px; + height: 16px; +} + +@media (max-width: 640px) { + .site-nav .nav-optional { + display: none; + } + + .site-nav .nav-github .nav-github-label { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + } + + .site-nav .nav-github { + padding: 0 var(--s-3); + } + + .wordmark .wordmark-sub { + display: none; + } +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: var(--s-2); + color: var(--text-3); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.08em; + line-height: 1.3; + text-transform: uppercase; +} + +h1 { + font-size: clamp(2.5rem, 4.2vw + 1rem, 4rem); + font-weight: 600; + line-height: 1.02; + letter-spacing: -0.045em; + text-wrap: balance; +} + +h2 { + font-size: clamp(1.75rem, 2vw + 1rem, 2.5rem); + font-weight: 600; + line-height: 1.1; + letter-spacing: -0.04em; + text-wrap: balance; +} + +h3 { + font-size: 1.25rem; + font-weight: 600; + line-height: 1.3; + letter-spacing: -0.02em; +} + +.lede { + color: var(--text-2); + font-size: clamp(1.0625rem, 0.4vw + 1rem, 1.25rem); + line-height: 1.7; + text-wrap: pretty; +} + +p { + text-wrap: pretty; +} + +.muted { + color: var(--text-2); +} + +.section-note { + margin-top: var(--s-6); +} + +:not(pre) > code { + padding: 0.12em 0.4em; + border-radius: var(--r-xs); + background: var(--surface-2); + font-size: 0.875em; +} + +h1 code { + padding: 0 0.14em 0.04em; + border-radius: var(--r-md); + background: var(--surface-2); + box-shadow: var(--e-1); + font-size: 0.8em; + font-weight: 500; + letter-spacing: -0.08em; + vertical-align: 0.06em; +} + +.text-link { + color: var(--hash); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; +} + +.text-link:hover { + text-decoration-thickness: 2px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--s-2); + min-height: 44px; + padding: 0 var(--s-5); + border: 0; + border-radius: var(--r-sm); + font: inherit; + font-size: 0.9375rem; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: transform 150ms ease, background-color 150ms ease, box-shadow 150ms ease; +} + +.btn:active { + transform: scale(0.98); +} + +.btn svg { + width: 16px; + height: 16px; +} + +.btn-primary { + background: var(--text); + color: var(--bg); +} + +.btn-primary:hover { + background: color-mix(in srgb, var(--text) 85%, var(--bg)); +} + +.btn-secondary { + background: var(--surface); + color: var(--text); + box-shadow: var(--e-1); +} + +.btn-secondary:hover { + box-shadow: 0 0 0 1px var(--ring-strong); +} + +.copy-cmd { + display: inline-flex; + align-items: center; + gap: var(--s-3); + min-height: 44px; + max-width: 100%; + padding: 0 var(--s-2) 0 var(--s-4); + border-radius: var(--r-sm); + background: var(--surface); + box-shadow: var(--e-2); +} + +.copy-cmd code { + min-width: 0; + overflow-x: auto; + padding: 0; + background: none; + font-size: 0.875rem; + white-space: nowrap; +} + +.copy-cmd .prompt { + color: var(--text-3); + user-select: none; +} + +.copy-btn { + position: relative; + display: inline-grid; + place-items: center; + flex: none; + width: 32px; + height: 32px; + padding: 0; + border: 0; + border-radius: var(--r-xs); + background: transparent; + color: var(--text-3); + cursor: pointer; + transition: background-color 150ms ease, color 150ms ease; +} + +.copy-btn:hover { + background: var(--surface-2); + color: var(--text); +} + +.copy-btn svg { + grid-area: 1 / 1; + width: 16px; + height: 16px; + transition: opacity 150ms ease, transform 150ms ease; +} + +.copy-btn .icon-check { + opacity: 0; + transform: scale(0.6); + color: var(--ok); +} + +.copy-btn[data-state="copied"] .icon-copy { + opacity: 0; + transform: scale(0.6); +} + +.copy-btn[data-state="copied"] .icon-check { + opacity: 1; + transform: none; +} + +.terminal .copy-btn { + color: var(--term-muted); +} + +.terminal .copy-btn:hover { + background: rgb(255 255 255 / 0.06); + color: var(--term-text); +} + +.hero { + position: relative; + isolation: isolate; + overflow: hidden; + padding-block: clamp(56px, 8vw, 112px) var(--section-y); +} + +.hero::before { + content: ""; + position: absolute; + inset: 0; + z-index: -2; + background-image: radial-gradient(var(--dot) 1px, transparent 1px); + background-size: 22px 22px; + mask-image: radial-gradient(ellipse 70% 60% at 70% 40%, #000 0%, transparent 75%); + -webkit-mask-image: radial-gradient(ellipse 70% 60% at 70% 40%, #000 0%, transparent 75%); +} + +.hero::after { + content: ""; + position: absolute; + z-index: -1; + top: 10%; + right: -10%; + width: min(70vw, 820px); + aspect-ratio: 1; + background: radial-gradient(closest-side, var(--glow), transparent); + pointer-events: none; +} + +.hero-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--s-8); + align-items: center; +} + +.hero-copy { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--s-5); + justify-items: start; + min-width: 0; + max-width: 600px; +} + +.hero-copy > * { + max-width: 100%; +} + +.hero-copy h1 { + margin-top: var(--s-1); +} + +.cta-row { + display: flex; + flex-wrap: wrap; + gap: var(--s-3); + margin-top: var(--s-2); +} + +.cta-row > * { + min-width: 0; + max-width: 100%; +} + +.badge-row { + display: flex; + flex-wrap: wrap; + gap: var(--s-2); +} + +.badge { + display: inline-flex; + align-items: center; + gap: var(--s-2); + min-height: 26px; + padding: 0 var(--s-3); + border-radius: var(--r-pill); + background: var(--surface-2); + color: var(--text-2); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.02em; +} + +.badge .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--ok); +} + +.hero-visual { + position: relative; + display: grid; + gap: var(--s-4); + min-width: 0; +} + +@media (min-width: 960px) { + .hero-grid { + grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); + gap: var(--s-8); + } +} + +.panel { + overflow: hidden; + border-radius: var(--r-lg); + background: var(--surface); + box-shadow: var(--e-3); +} + +.panel-bar { + display: flex; + align-items: center; + gap: var(--s-3); + min-height: 40px; + padding: 0 var(--s-4); + box-shadow: 0 1px 0 var(--ring); + color: var(--text-3); + font-family: var(--font-mono); + font-size: 0.75rem; +} + +.panel-bar .dots { + display: inline-flex; + gap: 6px; +} + +.panel-bar .dots span { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--ring-strong); +} + +.panel-bar .panel-title { + margin-left: auto; + margin-right: auto; + padding-right: 48px; +} + +.envfile-body { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + column-gap: var(--s-2); + row-gap: var(--s-2); + align-items: center; + padding: var(--s-5); + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.6; +} + +.envfile-body .env-key { + color: var(--text); +} + +.envfile-body .env-key::after { + content: "="; + color: var(--text-3); +} + +.redact { + display: block; + height: 1.05em; + width: var(--w, 70%); + border-radius: 3px; + background: + linear-gradient(180deg, rgb(255 255 255 / 0.1), transparent 55%), + var(--redact); + transform-origin: left center; + animation: redact-in 520ms var(--ease-out) both; + animation-delay: calc(250ms + var(--i, 0) * 90ms); +} + +@keyframes redact-in { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} + +.envfile-foot { + padding: var(--s-3) var(--s-5); + box-shadow: 0 -1px 0 var(--ring); + color: var(--text-3); + font-size: 0.8125rem; +} + +.terminal { + overflow: hidden; + border-radius: var(--r-lg); + background: var(--term-bg); + color: var(--term-text); + box-shadow: var(--e-term); +} + +.terminal .panel-bar { + background: var(--term-bar); + box-shadow: 0 1px 0 rgb(255 255 255 / 0.06); + color: var(--term-muted); +} + +.terminal .panel-bar .dots span { + background: rgb(255 255 255 / 0.14); +} + +.terminal-body { + overflow-x: auto; + padding: var(--s-5); + font-family: var(--font-mono); + font-size: 0.8125rem; + line-height: 1.75; + white-space: pre; +} + +.terminal-body:focus-visible { + outline-offset: -2px; +} + +@media (max-width: 640px) { + .terminal-body { + padding: var(--s-4); + font-size: 0.75rem; + } +} + +.ln { + display: block; +} + +.ln + .ln-cmd { + margin-top: var(--s-3); +} + +.ln-cmd { + color: var(--term-text); +} + +.ln-cmd .ps { + color: var(--term-muted); + user-select: none; +} + +.ln-out { + color: var(--term-muted); +} + +.tk-key { + color: var(--term-key); +} + +.tk-true { + color: var(--term-ok); +} + +.tk-hash { + color: var(--term-hash); +} + +.ln-block { + color: var(--term-block); +} + +.ln-ok { + color: var(--term-ok); +} + +.tag { + display: inline-block; + min-width: 7ch; + margin-right: 1ch; + font-weight: 500; +} + +.section { + padding-block: var(--section-y); +} + +.section-subtle { + background: var(--bg-subtle); + box-shadow: 0 -1px 0 var(--ring), 0 1px 0 var(--ring); +} + +.section-head { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--s-4); + max-width: 720px; + margin-bottom: var(--s-7); +} + +.grid { + display: grid; + gap: var(--s-4); +} + +@media (min-width: 640px) { + .grid-2, + .grid-3 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 960px) { + .grid-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .split { + display: grid; + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); + gap: var(--s-8); + align-items: start; + } + + .split .section-head { + position: sticky; + top: 112px; + margin-bottom: 0; + } +} + +.card { + display: grid; + grid-template-columns: minmax(0, 1fr); + align-content: start; + gap: var(--s-3); + padding: clamp(var(--s-5), 3vw, var(--s-6)); + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--e-2); +} + +.card p { + color: var(--text-2); + font-size: 0.9375rem; +} + +.card .glyph { + display: inline-grid; + place-items: center; + width: 36px; + height: 36px; + border-radius: var(--r-sm); + background: var(--surface-2); + color: var(--text); +} + +.card .glyph svg { + width: 18px; + height: 18px; +} + +.card-link { + text-decoration: none; + transition: box-shadow 200ms ease, transform 150ms ease; +} + +.card-link:hover { + box-shadow: var(--e-3), 0 0 0 1px var(--ring-strong); +} + +.card-link:active { + transform: scale(0.99); +} + +.card-link .more { + display: inline-flex; + align-items: center; + gap: var(--s-2); + margin-top: var(--s-2); + color: var(--text); + font-size: 0.875rem; + font-weight: 500; +} + +.card-link .more svg { + width: 14px; + height: 14px; + transition: transform 150ms ease; +} + +.card-link:hover .more svg { + transform: translateX(3px); +} + +.ops { + display: grid; + gap: 1px; + overflow: hidden; + border-radius: var(--r-md); + background: var(--ring); + box-shadow: var(--e-1); +} + +@media (min-width: 640px) { + .ops { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 960px) { + .ops { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +.op { + display: grid; + gap: var(--s-1); + padding: var(--s-5); + background: var(--surface); +} + +.op code { + justify-self: start; + font-size: 0.8125rem; + font-weight: 500; +} + +.op p { + color: var(--text-2); + font-size: 0.875rem; + line-height: 1.55; +} + +.op .needs { + color: var(--text-3); + font-family: var(--font-mono); + font-size: 0.6875rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.code-block { + overflow: hidden; + border-radius: var(--r-lg); + background: var(--term-bg); + color: var(--term-text); + box-shadow: var(--e-term); +} + +.code-block pre { + overflow-x: auto; + padding: var(--s-5); + font-size: 0.8125rem; + line-height: 1.75; +} + +.code-block pre:focus-visible { + outline-offset: -2px; +} + +.code-block .panel-bar { + background: var(--term-bar); + box-shadow: 0 1px 0 rgb(255 255 255 / 0.06); + color: var(--term-muted); +} + +.code-block .panel-bar .panel-title { + margin: 0; + padding: 0; +} + +.code-block .panel-bar .copy-btn { + margin-left: auto; +} + +.tk-kw { + color: #c792ea; +} + +.tk-str { + color: #a5d6a7; +} + +.tk-fn { + color: var(--term-hash); +} + +.tk-cm { + color: var(--term-muted); +} + +.stack { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--s-4); +} + +.table-wrap { + overflow-x: auto; + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--e-2); +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +.data-table th, +.data-table td { + padding: var(--s-3) var(--s-4); + text-align: left; + vertical-align: top; + box-shadow: inset 0 -1px 0 var(--ring); +} + +.data-table tr:last-child td { + box-shadow: none; +} + +.data-table th { + background: var(--surface-2); + color: var(--text-2); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + white-space: nowrap; +} + +.data-table td code { + white-space: nowrap; +} + +.data-table td.cmd { + white-space: nowrap; +} + +.check-list { + display: grid; + gap: var(--s-3); + padding: 0; + list-style: none; +} + +.check-list li { + display: grid; + grid-template-columns: 20px minmax(0, 1fr); + gap: var(--s-3); + color: var(--text-2); +} + +.check-list li strong { + color: var(--text); + font-weight: 500; +} + +.check-list svg { + width: 18px; + height: 18px; + margin-top: 4px; +} + +.check-list .is-block svg { + color: var(--block); +} + +.check-list .is-ok svg { + color: var(--ok); +} + +.check-list .is-note svg { + color: var(--text-3); +} + +.tool-group { + display: grid; + gap: var(--s-3); +} + +.tool-group h3 { + font-size: 1rem; +} + +.tool-list { + display: flex; + flex-wrap: wrap; + gap: var(--s-2); + padding: 0; + list-style: none; +} + +.tool-list code { + display: inline-block; + padding: var(--s-1) var(--s-2); + border-radius: var(--r-xs); + background: var(--surface-2); + font-size: 0.8125rem; +} + +.band { + display: grid; + gap: var(--s-5); + align-items: center; + padding: clamp(var(--s-6), 5vw, var(--s-8)); + border-radius: var(--r-lg); + background: var(--surface); + box-shadow: var(--e-3); +} + +@media (min-width: 960px) { + .band { + grid-template-columns: minmax(0, 1fr) auto; + } +} + +.band p { + color: var(--text-2); +} + +.site-footer { + padding-block: var(--s-8); + box-shadow: 0 -1px 0 var(--ring); + color: var(--text-3); + font-size: 0.875rem; +} + +.footer-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--s-5); +} + +.footer-links { + display: flex; + flex-wrap: wrap; + gap: var(--s-1) var(--s-5); + padding: 0; + list-style: none; +} + +.footer-links a { + display: inline-flex; + align-items: center; + min-height: 40px; + color: var(--text-2); + text-decoration: none; +} + +.footer-links a:hover { + color: var(--text); +} + +.not-found { + display: grid; + place-items: center; + min-height: calc(100dvh - 64px - 160px); + padding-block: var(--section-y); + text-align: center; +} + +.not-found .stack { + justify-items: center; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation: none !important; + transition: none !important; + } + + .redact { + transform: none; + } +} diff --git a/site/guard/index.html b/site/guard/index.html new file mode 100644 index 0000000..0dd40f4 --- /dev/null +++ b/site/guard/index.html @@ -0,0 +1,249 @@ + + + + + + envsitter-guard: keep AI agents out of your .env files + + + + + + + + + + + + + + + + + + +
+
+
+
+

OpenCode plugin

+

Keep AI agents out of your .env files.

+

envsitter-guard stops coding agents from reading or editing secret env files, then gives them fifteen envsitter tools that work with keys, fingerprints, and booleans instead of values.

+
+ OpenCode 2.0.32+ + OpenCode 1.18.29+ +
+
+
+ $ opencode plugin add envsitter-guard@latest + +
+ Other install options +
+
+ +
+
+
+ + opencode, agent session +
+
tool read .envblockedReading `.env*` is blocked to +prevent secret leaks (`.env`). +Use EnvSitter tools instead +(never prints values).tool shell cat .env.localblockedShell commands that reference +`.env*` files or +`.envsitter/pepper` are blocked +(`.env.local`).tool envsitter_keys {"filePath": ".env"}{ + "file": ".env", + "keys": [ + "API_TOKEN", + "DB_PASSWORD" + ] +}tool envsitter_match {"key": "NODE_ENV", + "op": "is_equal", ...}{ ..., "match": true }
+
+
+
+
+ +
+
+
+

Install

+

One package for both OpenCode versions.

+

OpenCode 2 loads the plugin through setup() and OpenCode 1 through server(), both from the same default export. Restart OpenCode 1 after changing config; OpenCode 2 reloads watched config on its own.

+
+
+
+
+ OpenCode 2: opencode.json + +
+
{
+  "$schema": "https://opencode.ai/config.json",
+  "plugins": ["envsitter-guard@latest"]
+}
+
+
+
+ OpenCode 1: opencode.json + +
+
{
+  "$schema": "https://opencode.ai/config.json",
+  "plugin": ["envsitter-guard@latest"]
+}
+
+
+

Prefer a local file? Re-export the package from .opencode/plugins/envsitter-guard.ts (OpenCode 2) or .opencode/plugin/ (OpenCode 1). The README has the snippet.

+
+
+ +
+
+
+

Blocking

+

Every path to a secret file, closed.

+

Protected files are .env, .env.<anything>, <name>.env, <name>.env.<anything>, and .envsitter/pepper. Blocked calls fail with a message that points the agent to envsitter_help.

+
+
+
    +
  • Reads and edits. Any non-envsitter tool whose path arguments point at a protected file: read, edit, write, grep on a file, MCP filesystem tools.
  • +
  • Patches. patch and apply_patch calls that add, update, move, or delete a protected file.
  • +
  • Shell commands. Commands that name a protected file, such as cat .env, source .env, echo X >> .env, or node --env-file=.env.
  • +
  • Search results. grep matches inside protected files are removed from the output.
  • +
  • Secret values in tool output. Secret-looking values from protected env files are replaced with [envsitter: KEY redacted] in every other tool's output, so grep -r, cat *, and log files do not hand them to the model.
  • +
  • Permission checks (OpenCode 2). read, edit, and shell checks on protected files are denied, including calls made from Code Mode.
  • +
+
    +
  • Templates stay readable. .env.example, .env.sample, .env.template, .env.dist, and .env.default(s).
  • +
  • The envsitter CLI still runs. Shell calls such as npx envsitter keys --file .env are allowed because the CLI never prints values.
  • +
+
+
+
+ +
+
+
+

Tools

+

Fifteen tools that never return a value.

+

Agents call them by name. Mutations and file operations are dry runs unless the agent passes write: true, and output is limited to keys, booleans, line numbers, and plans.

+
+
+
+

Reading

+
  • envsitter_keys
  • envsitter_fingerprint
  • envsitter_scan
+

List keys, fingerprint one value, or detect JWT, URL, and base64 shapes.

+
+
+

Matching

+
  • envsitter_match
  • envsitter_match_by_key
+

Boolean checks with every envsitter operator, for one key, many keys, or all keys.

+
+
+

Mutations

+
  • envsitter_add
  • envsitter_set
  • envsitter_unset
  • envsitter_delete
+

Add, update, empty, or remove keys without the agent ever reading the file.

+
+
+

File operations

+
  • envsitter_validate
  • envsitter_copy
  • envsitter_format
  • envsitter_reorder
  • envsitter_annotate
+

Validate syntax, copy keys between files, sort them, or add comments.

+
+
+

Help

+
  • envsitter_help
+

A built-in guide the agent can read when a call is blocked.

+
+
+
+
+ +
+
+
+

Compatibility

+

Verified against real OpenCode builds.

+

Before every release, an end-to-end check runs the real OpenCode 1 and OpenCode 2 CLIs against a scripted model and confirms each blocking scenario holds.

+
+
+
+ + + + + + +
OpenCodeMinimumVerifiedEntry point
2.x2.0.322.0.32, 2.1.0setup(ctx)
1.x1.18.291.18.31, 1.19.3server()
+
+
    +
  • A guardrail, not a sandbox. Shell commands that reach file contents without naming the file, such as grep -r TOKEN . or cat *, are not blocked; their output is scrubbed instead. Encoded or transformed values (base64, split strings) and values that do not look secret get through.
  • +
  • Default pepper location only. A pepper file at a custom path is not recognized as protected.
  • +
+
+
+
+ +
+
+
+
+

Built on envsitter

+

The same tools work outside OpenCode.

+

Every guard tool is backed by the envsitter library and CLI, which you can use directly in scripts, CI, and your own agent integrations.

+
+ Explore envsitter +
+
+
+
+ + + +

+ + diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..fd51972 --- /dev/null +++ b/site/index.html @@ -0,0 +1,283 @@ + + + + + + envsitter: inspect .env secrets without exposing values + + + + + + + + + + + + + + + + + + +
+
+
+
+

Open source library and CLI

+

Work with .env secrets without ever seeing them.

+

envsitter lists keys, fingerprints values with HMAC-SHA-256, and answers yes or no questions about them. Values stay in memory, so they never reach your terminal, your logs, or an AI agent's context.

+
+
+ $ npm install envsitter + +
+ Read the docs +
+
+ +
+
+
+ + .env, as envsitter reports it +
+
+ API_TOKEN + DATABASE_URL + NODE_ENV + PORT + STRIPE_SECRET_KEY +
+

Keys are readable. Values never leave memory.

+
+ +
+
+ + zsh +
+
$ envsitter keys --file .envAPI_TOKEN +DATABASE_URL +NODE_ENV +PORT +STRIPE_SECRET_KEY$ pbpaste | envsitter match \ + --file .env \ + --key STRIPE_SECRET_KEY \ + --candidate-stdin --json{ + "key": "STRIPE_SECRET_KEY", + "match": true +}
+
+
+
+
+ +
+
+
+

Match operators

+

Ask questions about a value. Get a boolean back.

+

Every operator evaluates the value in-process and returns only true or false. Equality is checked by comparing HMAC digests, never the raw strings.

+
+
+
exists

The key is present in the source.

+
is_empty

The value is exactly the empty string.

+
is_equalneeds a candidate

Deterministic match against a candidate you supply, compared in constant time.

+
partial_match_prefixneeds a candidate

The value starts with the candidate.

+
partial_match_suffixneeds a candidate

The value ends with the candidate.

+
partial_match_regexneeds a candidate

The value matches a regex such as /^sk-/.

+
is_number

The value parses as a finite number.

+
is_boolean

The value is true or false, ignoring case and surrounding spaces.

+
is_string

The value is neither a number nor a boolean.

+
+
+
+ +
+ +
+ +
+
+
+

CLI

+

Every command keeps values in memory.

+

Output contains keys, booleans, lengths, fingerprints, and line numbers. Commands that change files are dry runs until you pass --write.

+
+ $ npx envsitter keys --file .env + +
+

Exit codes: 0 success or match, 1 no match, 2 usage or runtime error.

+
+
+ + + + + + + + + + + + + + + + + + +
CommandWhat it does
keysList the keys in an env file
fingerprintDeterministic HMAC fingerprint of one key
matchMatch a candidate or operator against one, many, or all keys
match-by-keyBulk match a different candidate per key
scanDetect value shapes: JWT, URL, base64
redactReplace secret values in text read from stdin
validateCheck dotenv syntax with line and column
copyCopy keys between env files, with rename and conflict policy
format, reorderSort and organize keys, by section or globally
annotateAdd a comment above a key
addAdd a new key, failing if it already exists
setCreate or update a key
unsetSet a key to an empty value
deleteRemove one or more keys
+
+
+
+ +
+
+
+

Library

+

The same guarantees, typed.

+

Build envsitter into release scripts, audits, or your own agent tools. Nothing in the API returns a plaintext value.

+

Path helpers such as isSensitiveEnvFile tell your tooling which files hold real secrets and which are committed templates like .env.example.

+
+
+
+ check-secrets.ts + +
+
import { EnvSitter, setEnvFileKey } from 'envsitter';
+
+const es = EnvSitter.fromDotenvFile('.env');
+
+await es.listKeys();                              // ['API_TOKEN', 'PORT', ...]
+await es.matchCandidate('API_TOKEN', candidate);  // true or false
+await es.matchKey('PORT', { op: 'is_number' });   // true
+await es.fingerprintKey('API_TOKEN');             // hmac-sha256 digest + length
+
+// Dry run unless write: true. Returns a plan, never the value.
+await setEnvFileKey({ file: '.env', key: 'API_TOKEN', value: next, write: true });
+
+
+
+ +
+
+
+

Security model

+

Comparisons without disclosure.

+

Values are read in-process for comparisons, but the library never returns them and the CLI never prints them.

+
+
+
+ +

Keyed fingerprints

+

Fingerprints are HMAC-SHA-256 with a local pepper, not raw hashes that are easy to dictionary-guess. The pepper comes from ENVSITTER_PEPPER or a .envsitter/pepper file created with mode 0600.

+
+
+ +

Constant-time equality

+

is_equal hashes the candidate and the stored value with the same pepper and compares the digests in constant time.

+
+
+ +

Candidates over stdin

+

Pass candidates and new values with --candidate-stdin or --value-stdin so secrets stay out of your shell history.

+
+
+

Not a secret manager: envsitter does not encrypt, sync, or relocate values. It works on your env sources in place.

+
+
+ +
+
+
+
+

For AI coding agents

+

Give your agent envsitter, not your secrets.

+

envsitter-guard is an OpenCode plugin for OpenCode 1 and 2. It blocks agents from reading or editing secret env files and gives them fifteen envsitter tools that never return values.

+
+ Explore the guard +
+
+
+
+ + + +

+ +