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:
@@ -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
@@ -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.
|
||||
@@ -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.
@@ -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.
|
||||
@@ -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
@@ -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.<anything></code>, <code><name>.env</code>, <code><name>.env.<anything></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 >> .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
@@ -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>
|
||||
Reference in New Issue
Block a user