Add documentation site for envsitter and envsitter-guard

Static HTML and CSS with self-hosted Geist fonts and light/dark themes, deployed by pages.yml to https://boxpositron.github.io/envsitter/ (guard page at /guard/).
This commit is contained in:
David Ibia
2026-10-20 15:10:11 +01:00
parent aa2c8f2d6d
commit 33b4a72f2c
10 changed files with 1976 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page not found: envsitter</title>
<meta name="robots" content="noindex">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
<link rel="icon" href="/envsitter/assets/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/envsitter/assets/styles.css">
</head>
<body>
<main id="main" class="container not-found">
<div class="stack">
<p class="eyebrow">404</p>
<h1>This page is redacted.</h1>
<p class="lede">Nothing lives at this address. The value is not hidden, it just does not exist.</p>
<div class="cta-row">
<a class="btn btn-primary" href="/envsitter/">envsitter home</a>
<a class="btn btn-secondary" href="/envsitter/guard/">OpenCode guard</a>
</div>
</div>
</main>
</body>
</html>
+110
View File
@@ -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.
+18
View File
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<style>
.tile { fill: #171717; }
.key { fill: #ffffff; opacity: 0.5; }
.bar { fill: #ffffff; }
@media (prefers-color-scheme: dark) {
.tile { fill: #ededed; }
.key, .bar { fill: #0a0a0a; }
}
</style>
<rect class="tile" width="32" height="32" rx="8"/>
<rect class="key" x="7" y="10" width="5" height="3" rx="1.5"/>
<rect class="bar" x="14" y="10" width="11" height="3" rx="1.5"/>
<rect class="key" x="7" y="15.5" width="4" height="3" rx="1.5"/>
<rect class="bar" x="13" y="15.5" width="12" height="3" rx="1.5"/>
<rect class="key" x="7" y="21" width="6" height="3" rx="1.5"/>
<rect class="bar" x="15" y="21" width="7" height="3" rx="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.
Binary file not shown.
+92
View File
@@ -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.
+27
View File
@@ -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.");
}
});
});
})();
File diff suppressed because it is too large Load Diff
+249
View File
@@ -0,0 +1,249 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>envsitter-guard: keep AI agents out of your .env files</title>
<meta name="description" content="envsitter-guard is an OpenCode plugin for OpenCode 1 and 2 that blocks agents from reading or editing secret env files and gives them fifteen envsitter tools that never return values.">
<link rel="canonical" href="https://boxpositron.github.io/envsitter/guard/">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
<meta property="og:type" content="website">
<meta property="og:title" content="envsitter-guard: keep AI agents out of your .env files">
<meta property="og:description" content="An OpenCode plugin that blocks agents from reading or editing secret env files and gives them safe envsitter tools instead.">
<meta property="og:url" content="https://boxpositron.github.io/envsitter/guard/">
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">
<link rel="preload" href="../assets/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="../assets/styles.css">
<script src="../assets/site.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="container header-row">
<a class="wordmark" href="../">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="2.5" y="4.5" width="19" height="15" rx="3.5" stroke="currentColor" stroke-width="1.5"/><rect x="6" y="9" width="3.5" height="2" rx="1" fill="currentColor" opacity=".45"/><rect x="11" y="9" width="7" height="2" rx="1" fill="currentColor"/><rect x="6" y="13" width="2.5" height="2" rx="1" fill="currentColor" opacity=".45"/><rect x="10" y="13" width="8" height="2" rx="1" fill="currentColor"/></svg>
envsitter<span class="wordmark-sub">/guard</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a class="nav-optional" href="#install">Install</a>
<a class="nav-optional" href="#blocking">Blocking</a>
<a class="nav-optional" href="#tools">Tools</a>
<a href="./" aria-current="page">OpenCode guard</a>
<a class="nav-github" href="https://github.com/boxpositron/envsitter-guard">
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/></svg>
<span class="nav-github-label">GitHub</span>
</a>
</nav>
</div>
</header>
<main id="main">
<section class="hero" aria-labelledby="hero-title">
<div class="container hero-grid">
<div class="hero-copy">
<p class="eyebrow">OpenCode plugin</p>
<h1 id="hero-title">Keep AI agents out of your <code>.env</code> files.</h1>
<p class="lede">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.</p>
<div class="badge-row">
<span class="badge"><span class="dot" aria-hidden="true"></span>OpenCode 2.0.32+</span>
<span class="badge"><span class="dot" aria-hidden="true"></span>OpenCode 1.18.29+</span>
</div>
<div class="cta-row">
<div class="copy-cmd">
<code><span class="prompt">$ </span>opencode plugin add envsitter-guard@latest</code>
<button class="copy-btn" type="button" data-copy="opencode plugin add envsitter-guard@latest" aria-label="Copy opencode plugin add envsitter-guard@latest" hidden>
<svg class="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2.5"/><path d="M15.5 8.5V6A2.5 2.5 0 0 0 13 3.5H6A2.5 2.5 0 0 0 3.5 6v7A2.5 2.5 0 0 0 6 15.5h2.5"/></svg>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
</button>
</div>
<a class="btn btn-secondary" href="#install">Other install options</a>
</div>
</div>
<figure class="hero-visual" aria-label="An agent session where reading .env and running cat .env.local are blocked, and envsitter_keys returns only key names">
<div class="terminal">
<div class="panel-bar">
<span class="dots" aria-hidden="true"><span></span><span></span><span></span></span>
<span class="panel-title">opencode, agent session</span>
</div>
<div class="terminal-body" tabindex="0" role="region" aria-label="Agent session"><span class="ln ln-cmd"><span class="ps">tool </span>read .env</span><span class="ln ln-block"><span class="tag">blocked</span>Reading `.env*` is blocked to
<span class="tag"></span>prevent secret leaks (`.env`).
<span class="tag"></span>Use EnvSitter tools instead
<span class="tag"></span>(never prints values).</span><span class="ln ln-cmd"><span class="ps">tool </span>shell cat .env.local</span><span class="ln ln-block"><span class="tag">blocked</span>Shell commands that reference
<span class="tag"></span>`.env*` files or
<span class="tag"></span>`.envsitter/pepper` are blocked
<span class="tag"></span>(`.env.local`).</span><span class="ln ln-cmd"><span class="ps">tool </span>envsitter_keys {"filePath": ".env"}</span><span class="ln ln-out">{
<span class="tk-key">"file"</span>: ".env",
<span class="tk-key">"keys"</span>: [
"API_TOKEN",
"DB_PASSWORD"
]
}</span><span class="ln ln-cmd"><span class="ps">tool </span>envsitter_match {"key": "NODE_ENV",
"op": "is_equal", ...}</span><span class="ln ln-out">{ ..., <span class="tk-key">"match"</span>: <span class="tk-true">true</span> }</span></div>
</div>
</figure>
</div>
</section>
<section class="section section-subtle" id="install" aria-labelledby="install-title">
<div class="container">
<div class="section-head">
<p class="eyebrow">Install</p>
<h2 id="install-title">One package for both OpenCode versions.</h2>
<p class="lede">OpenCode 2 loads the plugin through <code>setup()</code> and OpenCode 1 through <code>server()</code>, both from the same default export. Restart OpenCode 1 after changing config; OpenCode 2 reloads watched config on its own.</p>
</div>
<div class="grid grid-2">
<div class="code-block">
<div class="panel-bar">
<span class="panel-title">OpenCode 2: opencode.json</span>
<button class="copy-btn" type="button" data-copy='{
"$schema": "https://opencode.ai/config.json",
"plugins": ["envsitter-guard@latest"]
}' aria-label="Copy OpenCode 2 config" hidden>
<svg class="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2.5"/><path d="M15.5 8.5V6A2.5 2.5 0 0 0 13 3.5H6A2.5 2.5 0 0 0 3.5 6v7A2.5 2.5 0 0 0 6 15.5h2.5"/></svg>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
</button>
</div>
<pre tabindex="0" role="region" aria-label="OpenCode 2 config"><code>{
<span class="tk-str">"$schema"</span>: <span class="tk-str">"https://opencode.ai/config.json"</span>,
<span class="tk-str">"plugins"</span>: [<span class="tk-str">"envsitter-guard@latest"</span>]
}</code></pre>
</div>
<div class="code-block">
<div class="panel-bar">
<span class="panel-title">OpenCode 1: opencode.json</span>
<button class="copy-btn" type="button" data-copy='{
"$schema": "https://opencode.ai/config.json",
"plugin": ["envsitter-guard@latest"]
}' aria-label="Copy OpenCode 1 config" hidden>
<svg class="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2.5"/><path d="M15.5 8.5V6A2.5 2.5 0 0 0 13 3.5H6A2.5 2.5 0 0 0 3.5 6v7A2.5 2.5 0 0 0 6 15.5h2.5"/></svg>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
</button>
</div>
<pre tabindex="0" role="region" aria-label="OpenCode 1 config"><code>{
<span class="tk-str">"$schema"</span>: <span class="tk-str">"https://opencode.ai/config.json"</span>,
<span class="tk-str">"plugin"</span>: [<span class="tk-str">"envsitter-guard@latest"</span>]
}</code></pre>
</div>
</div>
<p class="muted section-note">Prefer a local file? Re-export the package from <code>.opencode/plugins/envsitter-guard.ts</code> (OpenCode 2) or <code>.opencode/plugin/</code> (OpenCode 1). The <a class="text-link" href="https://github.com/boxpositron/envsitter-guard#readme">README</a> has the snippet.</p>
</div>
</section>
<section class="section" id="blocking" aria-labelledby="blocking-title">
<div class="container split">
<div class="section-head">
<p class="eyebrow">Blocking</p>
<h2 id="blocking-title">Every path to a secret file, closed.</h2>
<p class="lede">Protected files are <code>.env</code>, <code>.env.&lt;anything&gt;</code>, <code>&lt;name&gt;.env</code>, <code>&lt;name&gt;.env.&lt;anything&gt;</code>, and <code>.envsitter/pepper</code>. Blocked calls fail with a message that points the agent to <code>envsitter_help</code>.</p>
</div>
<div class="stack">
<ul class="check-list" aria-label="Blocked">
<li class="is-block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6m0-6-6 6"/></svg><span><strong>Reads and edits.</strong> Any non-envsitter tool whose path arguments point at a protected file: <code>read</code>, <code>edit</code>, <code>write</code>, <code>grep</code> on a file, MCP filesystem tools.</span></li>
<li class="is-block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6m0-6-6 6"/></svg><span><strong>Patches.</strong> <code>patch</code> and <code>apply_patch</code> calls that add, update, move, or delete a protected file.</span></li>
<li class="is-block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6m0-6-6 6"/></svg><span><strong>Shell commands.</strong> Commands that name a protected file, such as <code>cat .env</code>, <code>source .env</code>, <code>echo X &gt;&gt; .env</code>, or <code>node --env-file=.env</code>.</span></li>
<li class="is-block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6m0-6-6 6"/></svg><span><strong>Search results.</strong> <code>grep</code> matches inside protected files are removed from the output.</span></li>
<li class="is-block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6m0-6-6 6"/></svg><span><strong>Secret values in tool output.</strong> Secret-looking values from protected env files are replaced with <code>[envsitter: KEY redacted]</code> in every other tool's output, so <code>grep -r</code>, <code>cat *</code>, and log files do not hand them to the model.</span></li>
<li class="is-block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6m0-6-6 6"/></svg><span><strong>Permission checks (OpenCode 2).</strong> <code>read</code>, <code>edit</code>, and <code>shell</code> checks on protected files are denied, including calls made from Code Mode.</span></li>
</ul>
<ul class="check-list" aria-label="Allowed">
<li class="is-ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m8.5 12.5 2.5 2.5 4.5-5"/></svg><span><strong>Templates stay readable.</strong> <code>.env.example</code>, <code>.env.sample</code>, <code>.env.template</code>, <code>.env.dist</code>, and <code>.env.default(s)</code>.</span></li>
<li class="is-ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="m8.5 12.5 2.5 2.5 4.5-5"/></svg><span><strong>The envsitter CLI still runs.</strong> Shell calls such as <code>npx envsitter keys --file .env</code> are allowed because the CLI never prints values.</span></li>
</ul>
</div>
</div>
</section>
<section class="section section-subtle" id="tools" aria-labelledby="tools-title">
<div class="container">
<div class="section-head">
<p class="eyebrow">Tools</p>
<h2 id="tools-title">Fifteen tools that never return a value.</h2>
<p class="lede">Agents call them by name. Mutations and file operations are dry runs unless the agent passes <code>write: true</code>, and output is limited to keys, booleans, line numbers, and plans.</p>
</div>
<div class="grid grid-3">
<div class="card tool-group">
<h3>Reading</h3>
<ul class="tool-list"><li><code>envsitter_keys</code></li><li><code>envsitter_fingerprint</code></li><li><code>envsitter_scan</code></li></ul>
<p>List keys, fingerprint one value, or detect JWT, URL, and base64 shapes.</p>
</div>
<div class="card tool-group">
<h3>Matching</h3>
<ul class="tool-list"><li><code>envsitter_match</code></li><li><code>envsitter_match_by_key</code></li></ul>
<p>Boolean checks with every envsitter operator, for one key, many keys, or all keys.</p>
</div>
<div class="card tool-group">
<h3>Mutations</h3>
<ul class="tool-list"><li><code>envsitter_add</code></li><li><code>envsitter_set</code></li><li><code>envsitter_unset</code></li><li><code>envsitter_delete</code></li></ul>
<p>Add, update, empty, or remove keys without the agent ever reading the file.</p>
</div>
<div class="card tool-group">
<h3>File operations</h3>
<ul class="tool-list"><li><code>envsitter_validate</code></li><li><code>envsitter_copy</code></li><li><code>envsitter_format</code></li><li><code>envsitter_reorder</code></li><li><code>envsitter_annotate</code></li></ul>
<p>Validate syntax, copy keys between files, sort them, or add comments.</p>
</div>
<div class="card tool-group">
<h3>Help</h3>
<ul class="tool-list"><li><code>envsitter_help</code></li></ul>
<p>A built-in guide the agent can read when a call is blocked.</p>
</div>
</div>
</div>
</section>
<section class="section" id="compatibility" aria-labelledby="compat-title">
<div class="container split">
<div class="section-head">
<p class="eyebrow">Compatibility</p>
<h2 id="compat-title">Verified against real OpenCode builds.</h2>
<p class="lede">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.</p>
</div>
<div class="stack">
<div class="table-wrap" tabindex="0" role="region" aria-label="Compatibility">
<table class="data-table">
<thead><tr><th scope="col">OpenCode</th><th scope="col">Minimum</th><th scope="col">Verified</th><th scope="col">Entry point</th></tr></thead>
<tbody>
<tr><td>2.x</td><td><code>2.0.32</code></td><td><code>2.0.32</code>, <code>2.1.0</code></td><td><code>setup(ctx)</code></td></tr>
<tr><td>1.x</td><td><code>1.18.29</code></td><td><code>1.18.31</code>, <code>1.19.3</code></td><td><code>server()</code></td></tr>
</tbody>
</table>
</div>
<ul class="check-list" aria-label="Limits">
<li class="is-note"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 11v5M12 8h.01"/></svg><span><strong>A guardrail, not a sandbox.</strong> Shell commands that reach file contents without naming the file, such as <code>grep -r TOKEN .</code> or <code>cat *</code>, are not blocked; their output is scrubbed instead. Encoded or transformed values (base64, split strings) and values that do not look secret get through.</span></li>
<li class="is-note"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 11v5M12 8h.01"/></svg><span><strong>Default pepper location only.</strong> A pepper file at a custom path is not recognized as protected.</span></li>
</ul>
</div>
</div>
</section>
<section class="section section-subtle" aria-labelledby="library-title">
<div class="container">
<div class="band">
<div class="stack">
<p class="eyebrow">Built on envsitter</p>
<h2 id="library-title">The same tools work outside OpenCode.</h2>
<p>Every guard tool is backed by the envsitter library and CLI, which you can use directly in scripts, CI, and your own agent integrations.</p>
</div>
<a class="btn btn-primary" href="../">Explore envsitter <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container footer-row">
<p>envsitter-guard is open source under the MIT license.</p>
<ul class="footer-links">
<li><a href="https://github.com/boxpositron/envsitter-guard">GitHub</a></li>
<li><a href="https://www.npmjs.com/package/envsitter-guard">npm</a></li>
<li><a href="https://github.com/boxpositron/envsitter-guard/blob/main/CHANGELOG.md">Changelog</a></li>
<li><a href="../">envsitter</a></li>
</ul>
</div>
</footer>
<p id="copy-status" class="visually-hidden" aria-live="polite"></p>
</body>
</html>
+283
View File
@@ -0,0 +1,283 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>envsitter: inspect .env secrets without exposing values</title>
<meta name="description" content="envsitter is a TypeScript library and CLI that lists keys, fingerprints values with HMAC-SHA-256, and answers yes or no questions about .env secrets without ever printing them.">
<link rel="canonical" href="https://boxpositron.github.io/envsitter/">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
<meta property="og:type" content="website">
<meta property="og:title" content="envsitter: inspect .env secrets without exposing values">
<meta property="og:description" content="List keys, fingerprint values, and match candidates against .env secrets without ever printing them.">
<meta property="og:url" content="https://boxpositron.github.io/envsitter/">
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
<link rel="preload" href="assets/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="assets/styles.css">
<script src="assets/site.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="container header-row">
<a class="wordmark" href="./">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="2.5" y="4.5" width="19" height="15" rx="3.5" stroke="currentColor" stroke-width="1.5"/><rect x="6" y="9" width="3.5" height="2" rx="1" fill="currentColor" opacity=".45"/><rect x="11" y="9" width="7" height="2" rx="1" fill="currentColor"/><rect x="6" y="13" width="2.5" height="2" rx="1" fill="currentColor" opacity=".45"/><rect x="10" y="13" width="8" height="2" rx="1" fill="currentColor"/></svg>
envsitter
</a>
<nav class="site-nav" aria-label="Primary">
<a class="nav-optional" href="#cli">CLI</a>
<a class="nav-optional" href="#library">Library</a>
<a href="guard/">OpenCode guard</a>
<a class="nav-github" href="https://github.com/boxpositron/envsitter">
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/></svg>
<span class="nav-github-label">GitHub</span>
</a>
</nav>
</div>
</header>
<main id="main">
<section class="hero" aria-labelledby="hero-title">
<div class="container hero-grid">
<div class="hero-copy">
<p class="eyebrow">Open source library and CLI</p>
<h1 id="hero-title">Work with <code>.env</code> secrets without ever seeing them.</h1>
<p class="lede">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.</p>
<div class="cta-row">
<div class="copy-cmd">
<code><span class="prompt">$ </span>npm install envsitter</code>
<button class="copy-btn" type="button" data-copy="npm install envsitter" aria-label="Copy npm install envsitter" hidden>
<svg class="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2.5"/><path d="M15.5 8.5V6A2.5 2.5 0 0 0 13 3.5H6A2.5 2.5 0 0 0 3.5 6v7A2.5 2.5 0 0 0 6 15.5h2.5"/></svg>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
</button>
</div>
<a class="btn btn-secondary" href="https://github.com/boxpositron/envsitter#readme">Read the docs</a>
</div>
</div>
<figure class="hero-visual" aria-label="An env file whose values are redacted, next to envsitter terminal output that shows only keys and a boolean match">
<div class="panel">
<div class="panel-bar">
<span class="dots" aria-hidden="true"><span></span><span></span><span></span></span>
<span class="panel-title">.env, as envsitter reports it</span>
</div>
<div class="envfile-body">
<span class="env-key">API_TOKEN</span><span class="redact" style="--w: 78%; --i: 0" role="img" aria-label="value hidden"></span>
<span class="env-key">DATABASE_URL</span><span class="redact" style="--w: 94%; --i: 1" role="img" aria-label="value hidden"></span>
<span class="env-key">NODE_ENV</span><span class="redact" style="--w: 34%; --i: 2" role="img" aria-label="value hidden"></span>
<span class="env-key">PORT</span><span class="redact" style="--w: 16%; --i: 3" role="img" aria-label="value hidden"></span>
<span class="env-key">STRIPE_SECRET_KEY</span><span class="redact" style="--w: 70%; --i: 4" role="img" aria-label="value hidden"></span>
</div>
<p class="envfile-foot">Keys are readable. Values never leave memory.</p>
</div>
<div class="terminal">
<div class="panel-bar">
<span class="dots" aria-hidden="true"><span></span><span></span><span></span></span>
<span class="panel-title">zsh</span>
</div>
<div class="terminal-body" tabindex="0" role="region" aria-label="Terminal output"><span class="ln ln-cmd"><span class="ps">$ </span>envsitter keys --file .env</span><span class="ln ln-out">API_TOKEN
DATABASE_URL
NODE_ENV
PORT
STRIPE_SECRET_KEY</span><span class="ln ln-cmd"><span class="ps">$ </span>pbpaste | envsitter match \
--file .env \
--key STRIPE_SECRET_KEY \
--candidate-stdin --json</span><span class="ln ln-out">{
<span class="tk-key">"key"</span>: "STRIPE_SECRET_KEY",
<span class="tk-key">"match"</span>: <span class="tk-true">true</span>
}</span></div>
</div>
</figure>
</div>
</section>
<section class="section section-subtle" aria-labelledby="ops-title">
<div class="container">
<div class="section-head">
<p class="eyebrow">Match operators</p>
<h2 id="ops-title">Ask questions about a value. Get a boolean back.</h2>
<p class="lede">Every operator evaluates the value in-process and returns only <code>true</code> or <code>false</code>. Equality is checked by comparing HMAC digests, never the raw strings.</p>
</div>
<div class="ops">
<div class="op"><code>exists</code><p>The key is present in the source.</p></div>
<div class="op"><code>is_empty</code><p>The value is exactly the empty string.</p></div>
<div class="op"><code>is_equal</code><span class="needs">needs a candidate</span><p>Deterministic match against a candidate you supply, compared in constant time.</p></div>
<div class="op"><code>partial_match_prefix</code><span class="needs">needs a candidate</span><p>The value starts with the candidate.</p></div>
<div class="op"><code>partial_match_suffix</code><span class="needs">needs a candidate</span><p>The value ends with the candidate.</p></div>
<div class="op"><code>partial_match_regex</code><span class="needs">needs a candidate</span><p>The value matches a regex such as <code>/^sk-/</code>.</p></div>
<div class="op"><code>is_number</code><p>The value parses as a finite number.</p></div>
<div class="op"><code>is_boolean</code><p>The value is <code>true</code> or <code>false</code>, ignoring case and surrounding spaces.</p></div>
<div class="op"><code>is_string</code><p>The value is neither a number nor a boolean.</p></div>
</div>
</div>
</section>
<section class="section" aria-labelledby="surfaces-title">
<div class="container">
<div class="section-head">
<p class="eyebrow">One rule everywhere</p>
<h2 id="surfaces-title">Keys, fingerprints, and booleans. Never values.</h2>
<p class="lede">The same guarantee holds whether you run a command, call the library, or let an AI coding agent work in your project.</p>
</div>
<div class="grid grid-3">
<a class="card card-link" href="#cli">
<span class="glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="3.5" width="19" height="17" rx="2.5"/><path d="m7 9 3 3-3 3M12.5 15h4.5"/></svg></span>
<h3>CLI</h3>
<p>Inspect, validate, copy, and edit env files from your shell with JSON output and useful exit codes.</p>
<span class="more">Command reference <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</a>
<a class="card card-link" href="#library">
<span class="glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M8 4C6 4 5.5 5 5.5 7v2c0 1.5-.8 2.5-2 3 1.2.5 2 1.5 2 3v2c0 2 .5 3 2.5 3M16 4c2 0 2.5 1 2.5 3v2c0 1.5.8 2.5 2 3-1.2.5-2 1.5-2 3v2c0 2-.5 3-2.5 3"/></svg></span>
<h3>Library</h3>
<p>A typed TypeScript API for keys, fingerprints, matchers, scans, and file operations inside your own tools.</p>
<span class="more">Library usage <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</a>
<a class="card card-link" href="guard/">
<span class="glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 4.5 6v5.5c0 4.6 3.1 8.2 7.5 9.5 4.4-1.3 7.5-4.9 7.5-9.5V6L12 3Z"/><path d="m9 12 2 2 4-4"/></svg></span>
<h3>OpenCode guard</h3>
<p>A plugin that blocks AI agents from reading or editing secret env files and hands them envsitter tools instead.</p>
<span class="more">Guard for agents <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</a>
</div>
</div>
</section>
<section class="section section-subtle" id="cli" aria-labelledby="cli-title">
<div class="container split">
<div class="section-head">
<p class="eyebrow">CLI</p>
<h2 id="cli-title">Every command keeps values in memory.</h2>
<p class="lede">Output contains keys, booleans, lengths, fingerprints, and line numbers. Commands that change files are dry runs until you pass <code>--write</code>.</p>
<div class="copy-cmd">
<code><span class="prompt">$ </span>npx envsitter keys --file .env</code>
<button class="copy-btn" type="button" data-copy="npx envsitter keys --file .env" aria-label="Copy npx envsitter keys --file .env" hidden>
<svg class="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2.5"/><path d="M15.5 8.5V6A2.5 2.5 0 0 0 13 3.5H6A2.5 2.5 0 0 0 3.5 6v7A2.5 2.5 0 0 0 6 15.5h2.5"/></svg>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
</button>
</div>
<p class="muted">Exit codes: <code>0</code> success or match, <code>1</code> no match, <code>2</code> usage or runtime error.</p>
</div>
<div class="table-wrap" tabindex="0" role="region" aria-label="CLI commands">
<table class="data-table">
<thead><tr><th scope="col">Command</th><th scope="col">What it does</th></tr></thead>
<tbody>
<tr><td class="cmd"><code>keys</code></td><td>List the keys in an env file</td></tr>
<tr><td class="cmd"><code>fingerprint</code></td><td>Deterministic HMAC fingerprint of one key</td></tr>
<tr><td class="cmd"><code>match</code></td><td>Match a candidate or operator against one, many, or all keys</td></tr>
<tr><td class="cmd"><code>match-by-key</code></td><td>Bulk match a different candidate per key</td></tr>
<tr><td class="cmd"><code>scan</code></td><td>Detect value shapes: JWT, URL, base64</td></tr>
<tr><td class="cmd"><code>redact</code></td><td>Replace secret values in text read from stdin</td></tr>
<tr><td class="cmd"><code>validate</code></td><td>Check dotenv syntax with line and column</td></tr>
<tr><td class="cmd"><code>copy</code></td><td>Copy keys between env files, with rename and conflict policy</td></tr>
<tr><td class="cmd"><code>format</code>, <code>reorder</code></td><td>Sort and organize keys, by section or globally</td></tr>
<tr><td class="cmd"><code>annotate</code></td><td>Add a comment above a key</td></tr>
<tr><td class="cmd"><code>add</code></td><td>Add a new key, failing if it already exists</td></tr>
<tr><td class="cmd"><code>set</code></td><td>Create or update a key</td></tr>
<tr><td class="cmd"><code>unset</code></td><td>Set a key to an empty value</td></tr>
<tr><td class="cmd"><code>delete</code></td><td>Remove one or more keys</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="section" id="library" aria-labelledby="library-title">
<div class="container">
<div class="section-head">
<p class="eyebrow">Library</p>
<h2 id="library-title">The same guarantees, typed.</h2>
<p class="lede">Build envsitter into release scripts, audits, or your own agent tools. Nothing in the API returns a plaintext value.</p>
<p class="muted">Path helpers such as <code>isSensitiveEnvFile</code> tell your tooling which files hold real secrets and which are committed templates like <code>.env.example</code>.</p>
</div>
<div class="code-block">
<div class="panel-bar">
<span class="panel-title">check-secrets.ts</span>
<button class="copy-btn" type="button" data-copy="import { EnvSitter, setEnvFileKey } from 'envsitter';
const es = EnvSitter.fromDotenvFile('.env');
await es.listKeys();
await es.matchCandidate('API_TOKEN', candidate);
await es.matchKey('PORT', { op: 'is_number' });
await es.fingerprintKey('API_TOKEN');
await setEnvFileKey({ file: '.env', key: 'API_TOKEN', value: next, write: true });" aria-label="Copy library example" hidden>
<svg class="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2.5"/><path d="M15.5 8.5V6A2.5 2.5 0 0 0 13 3.5H6A2.5 2.5 0 0 0 3.5 6v7A2.5 2.5 0 0 0 6 15.5h2.5"/></svg>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
</button>
</div>
<pre tabindex="0" role="region" aria-label="Library example"><code><span class="tk-kw">import</span> { EnvSitter, setEnvFileKey } <span class="tk-kw">from</span> <span class="tk-str">'envsitter'</span>;
<span class="tk-kw">const</span> es = EnvSitter.<span class="tk-fn">fromDotenvFile</span>(<span class="tk-str">'.env'</span>);
<span class="tk-kw">await</span> es.<span class="tk-fn">listKeys</span>(); <span class="tk-cm">// ['API_TOKEN', 'PORT', ...]</span>
<span class="tk-kw">await</span> es.<span class="tk-fn">matchCandidate</span>(<span class="tk-str">'API_TOKEN'</span>, candidate); <span class="tk-cm">// true or false</span>
<span class="tk-kw">await</span> es.<span class="tk-fn">matchKey</span>(<span class="tk-str">'PORT'</span>, { op: <span class="tk-str">'is_number'</span> }); <span class="tk-cm">// true</span>
<span class="tk-kw">await</span> es.<span class="tk-fn">fingerprintKey</span>(<span class="tk-str">'API_TOKEN'</span>); <span class="tk-cm">// hmac-sha256 digest + length</span>
<span class="tk-cm">// Dry run unless write: true. Returns a plan, never the value.</span>
<span class="tk-kw">await</span> <span class="tk-fn">setEnvFileKey</span>({ file: <span class="tk-str">'.env'</span>, key: <span class="tk-str">'API_TOKEN'</span>, value: next, write: <span class="tk-kw">true</span> });</code></pre>
</div>
</div>
</section>
<section class="section section-subtle" aria-labelledby="security-title">
<div class="container">
<div class="section-head">
<p class="eyebrow">Security model</p>
<h2 id="security-title">Comparisons without disclosure.</h2>
<p class="lede">Values are read in-process for comparisons, but the library never returns them and the CLI never prints them.</p>
</div>
<div class="grid grid-3">
<div class="card">
<span class="glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4 7 20M17 4l-2 16M4.5 9h16M3.5 15h16"/></svg></span>
<h3>Keyed fingerprints</h3>
<p>Fingerprints are HMAC-SHA-256 with a local pepper, not raw hashes that are easy to dictionary-guess. The pepper comes from <code>ENVSITTER_PEPPER</code> or a <code>.envsitter/pepper</code> file created with mode 0600.</p>
</div>
<div class="card">
<span class="glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="7" width="19" height="10" rx="5"/><circle cx="16.5" cy="12" r="2.5"/></svg></span>
<h3>Constant-time equality</h3>
<p><code>is_equal</code> hashes the candidate and the stored value with the same pepper and compares the digests in constant time.</p>
</div>
<div class="card">
<span class="glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="3.5" width="19" height="17" rx="2.5"/><path d="m7 9 3 3-3 3M12.5 15h4.5"/></svg></span>
<h3>Candidates over stdin</h3>
<p>Pass candidates and new values with <code>--candidate-stdin</code> or <code>--value-stdin</code> so secrets stay out of your shell history.</p>
</div>
</div>
<p class="muted section-note">Not a secret manager: envsitter does not encrypt, sync, or relocate values. It works on your env sources in place.</p>
</div>
</section>
<section class="section" aria-labelledby="guard-title">
<div class="container">
<div class="band">
<div class="stack">
<p class="eyebrow">For AI coding agents</p>
<h2 id="guard-title">Give your agent envsitter, not your secrets.</h2>
<p>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.</p>
</div>
<a class="btn btn-primary" href="guard/">Explore the guard <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container footer-row">
<p>envsitter is open source under the MIT license.</p>
<ul class="footer-links">
<li><a href="https://github.com/boxpositron/envsitter">GitHub</a></li>
<li><a href="https://www.npmjs.com/package/envsitter">npm</a></li>
<li><a href="https://github.com/boxpositron/envsitter/blob/main/CHANGELOG.md">Changelog</a></li>
<li><a href="guard/">OpenCode guard</a></li>
</ul>
</div>
</footer>
<p id="copy-status" class="visually-hidden" aria-live="polite"></p>
</body>
</html>