個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Local-first secrets for Node and Bun projects
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Local-first secrets for Node and Bun projects
Your .env files hold only references (kerstel:// /) — safe to read, grep, and commit. The real values live in an encrypted vault on your machine. No account, no cloud, no telemetry.檢視完整 README ↗Local-first secrets for Node and Bun projects
Your .env files hold only references (kerstel://<scope>/<KEY>) — safe to read, grep, and commit. The real values live in an encrypted vault on your machine. No account, no cloud, no telemetry.
Problem · How it works · Usage · Security · Build from source
.env and .env.local files hold secrets in plaintext. Anything that can read files — AI coding agents, editor plugins, accidental commits, backup tools — can read those secrets too. Existing solutions solve this with cloud accounts and a wrapper command you have to remember to type. Kerstel has no account, and writes its wrapper into your package.json scripts once so you never type it: npm run dev stays npm run dev.
Secrets are stored once, encrypted, in a local vault:
ks set global/OPENAI_API_KEY --value sk-...
Your .env file then holds a reference instead of the value:
# .env — safe to commit
OPENAI_API_KEY=kerstel://global/OPENAI_API_KEY
A reference names exactly one scope — global, or a project name — with no fallback chain. A resolver daemon unlocks the vault once via your OS credential store (Keychain on macOS, Secret Service on Linux) and serves resolutions to your app's process over a local socket. Your code sees the real value in process.env; the file on disk never does.
ks init [--yes] [--dry-run] # Migrate this project's .env files
ks move [KEY...] [--to global|project|plaintext] [--yes] [--replace] [--allow-tracked]
# Move keys between the vault and plain text
ks set <scope>/<KEY> [--value <value>] # Store a secret (or pipe it on stdin)
ks get <scope>/<KEY> [--reveal] # Read a secret
ks ls [--scope <scope>] # List stored references
ks rm <scope>/<KEY> --yes # Remove a secret
ks run -- <command> # Run a command with references resolved
ks exec -- <command> # Run a command with the hook wired in
ks resolve kerstel://<scope>/<KEY> # Print one resolved value
ks daemon <serve|start|stop|status> # Manage the resolver daemon
ks doctor [--verbose] # Diagnose this machine's setup
ks update [--check] # Install the latest release
ks uninstall [--dry-run] [--yes] [--force] # Restore every project, then remove Kerstel
ks --version # Print the version, and whether it's current
ks run -- <command> is the universal fallback: it resolves every reference in the current environment up front and execs the command with plaintext values injected. It works for anything that can't load the runtime hook, such as IDE run configurations. Projects wired up with the runtime hook resolve references lazily instead, straight out of process.env. Those projects still go through a wrapper — kerstel exec — but kerstel init writes it into your package.json scripts once, so you never type it: npm run dev is still npm run dev.
curl -fsSL https://kerstel.dev/install.sh | bash
macOS and Linux, x64 and arm64. The installer verifies the release checksum and puts the binary at ~/.local/bin/kerstel, without sudo. Run ks update to upgrade in place: it shows the download's progress, verifies the checksum, and only then swaps the binary. Set KERSTEL_VERSION=0.1.0 when installing to pin a version. ks --version and ks doctor tell you when a newer release exists.
The installer also adds ks, a shortcut for kerstel. If something else on your PATH is already called ks, it leaves that alone and tells you to use kerstel instead. Everything below works the same either way — ks and kerstel are the same binary.
To remove Kerstel, run ks uninstall. It rewrites every project's references back to their values, unwraps your scripts, and then deletes ~/.kerstel, the vault key, the binary, and every kerstel or ks link that points at it, in that order. Values go back in the quoting you wrote them in. It refuses if a secret would be lost, and names it: that includes a value init kept only in its encrypted backup, when a key had different values in several .env files, and a value ks move saved in its backup that is now nowhere else. --force goes ahead anyway. If git tracks a restored .env file, it tells you to run git rm --cached on it. On a machine with no Kerstel data, it just removes the binary.
cd my-app
ks init
The wizard shows every variable it found, grouped by where it suggests putting it — the vault for this project, the vault shared across all your projects, or left as plain text — and then asks "Look right?": press Enter to accept every suggestion, or choose Let me change some to pick individual keys from a checklist, or Go through them one by one to answer for each in turn. Once you accept, it asks whether to touch .gitignore, if that file hides your env files. Then it shows a one-line summary per file of what will change and asks "Apply these changes?" once, before its first write, covering all of it: the backup, the vault entries, the .env rewrites, the wiring and any .gitignore edit. Press Enter to apply, choose Show the full diff first to see every file's masked diff before answering, or Cancel to write nothing.
.env / .env.* file in the project root (templates like .env.example are skipped, and so are backup copies like .env.bak or .env.swp, which it names so you can delete them, or rename one that is a real env file) and show what it found: key names and sources, with each value shown only as its length. The exception is a configuration value that stays in plain text, like PORT=3000 or NODE_ENV=development, which is shown as it is. A name that says it is a credential wins over a public prefix, so VITE_SUPABASE_KEY is shown as a length, and so is a value that looks like a token or a URL with a credential in it. For each key you choose: store it in this project's scope, point it at a global key shared across all your projects, or leave it as plaintext (right for NODE_ENV, ports and public URLs). A line the wizard cannot read as KEY=value is left as it is and reported: by its key when it has one, otherwise by file and line, never by its text. A private key pasted unquoted across several lines (-----BEGIN … -----END) counts as one such value; store it with ks set and reference it, or put it on one line in double quotes with \n.~/.kerstel/backups/<project>/<timestamp>/..kerstel/exec.cjs (commit it), and every command in every package.json script gets node .kerstel/exec.cjs -- in front of it, after any leading NAME=value words, so node copy.mjs && next dev becomes node .kerstel/exec.cjs -- node copy.mjs && node .kerstel/exec.cjs -- next dev. The launcher runs kerstel exec where Kerstel is installed and the command unchanged where it is not, so a deploy host needs nothing (npm lifecycle hooks are never wrapped, nor are commands that never run JavaScript, such as rm or echo; a script with cd, a redirection, or shell control is skipped and named). Show the full diff first lets you see it before anything is written. Bun projects are wired the same way — kerstel exec passes to itself, because Bun ignores .Afterwards npm run dev is still npm run dev.
Useful flags:
| Flag | What it does |
|---|---|
--dry-run | Prints every diff and writes nothing: not to your project, your vault, or ~/.kerstel. Run this first. |
--yes | Accepts every suggestion, asks nothing. |
--scope <name> | Overrides the project scope (default: your package.json name). |
--global KEY[,KEY] | Forces those keys into the global scope. |
--keep KEY[,KEY] | Forces those keys to stay plaintext. |
--non-interactive | Never asks a question. Applies the suggested plan like --yes, but fails with exit 2 naming the flag on any question no flag can answer — a missing secret value, for instance. Pair it with --from-stdin in scripts. |
--from-stdin | Reads {"KEY": "value"} JSON for keys this machine is missing. |
ks init is idempotent: run it again after adding a key and it migrates only what is new.
If the same key appears in more than one file with different values, Kerstel stores the highest-precedence one — .env.<x>.local beats .env.local beats .env.<x> beats .env — points every occurrence at that one reference, and tells you which files it collapsed. The other values remain in the encrypted backup. Kerstel has no environments yet (they are on the roadmap), so one key resolves to one value.
git clone [email protected]:acme/my-app.git && cd my-app
ks init
The committed .env holds references, so init lists the keys your vault does not have yet and prompts for each one with the echo turned off. It stores them when you apply the plan, or straight away if nothing else in the project needs changing. The references double as a living .env.example. To supply them from a script instead:
echo '{"DATABASE_URL":"postgres://...","STRIPE_SECRET_KEY":"sk_live_..."}' | kerstel init --from-stdin --non-interactive
init never accepts a secret as a command-line argument. The one command that will is ks set --value, and it warns you: anything on argv is in your shell history and in ps output, so pipe the value on stdin instead.
kerstel exec vs kerstel runkerstel exec -- <cmd> | kerstel run -- <cmd> | |
|---|---|---|
| What the child's environment holds | references, untouched | resolved plaintext |
| When values are resolved | lazily, on each process.env read, through the daemon; under Bun also once at startup, so Bun.env sees the value | all at once, before the command starts |
| What it needs | the runtime hook (Node or Bun) | nothing |
| Audit log | one row per key the app actually reads (under Bun, one per reference) | one row per reference in the environment |
| Used by | your wired package.json scripts, through the committed launcher | IDE run configurations, other languages, anything the hook cannot reach |
| Where Kerstel is absent | the launcher runs the command unchanged, with one stderr line | fails |
exec is what the wizard writes into your scripts, behind the launcher; run is the universal fallback that always works. Deploy hosts need neither: the launcher runs the command unchanged where Kerstel is absent, and the values come from the host's own settings. See Deploying. Both tell you when the command itself is not on PATH, and in a project name the install command for your package manager, since in a fresh clone the missing binary is usually a dependency you haven't installed yet. They exit 127 in that case, as a shell does.
.env yields only references.KERSTEL_KEYCHAIN_BACKEND=file, as base64 in ~/.kerstel/vault.key (0600), which is then the file to protect. If Kerstel falls back to that file without being asked to, every command that opens the vault says so on stderr. It is never replaced by accident: a new key is minted only on a machine with no vault sealed with one, and a store that fails to answer is treated as holding a key, so you get an error naming the store rather than a fresh key over the real one. Kerstel talks to that store through the OS's own tool (security, secret-tool), run only from a root-owned system directory such as /usr/bin, never from PATH, so a dependency's node_modules/.bin cannot stand in for it. For the same reason init and exec refuse, and doctor reports a problem, when node_modules/.bin holds a kerstel of its own.KERSTEL_* settings are never read from a project's env files. Kerstel is a Bun binary, so it loads the .env of the directory you run it in, and that file is committed by design; a cloned repository could otherwise choose your vault's location or your credential store. A KERSTEL_* that an env file there names is dropped and named on stderr, whatever its value, so a setting you made yourself is ignored too while that line is in the file. The daemon runs from ~/.kerstel, so no env file reaches it.ks exec gives a wired script KERSTEL_TOKEN_FILE, the path of the 0600 token file, and the runtime hook reads the file when it needs it, so nothing a child process inherits, prints, or leaves in a crash report unlocks the vault. The daemon mints a fresh token each time it starts and removes it when it stops.PATH, HOME, locale, the Linux session bus, and Kerstel's KERSTEL_* settings), never the caller's, whether it is started on demand or you run ks daemon serve yourself. A BUN_OPTIONS or NODE_OPTIONS in your shell, and any plaintext an outer script had already resolved, never reach the process that holds the vault key. ks doctor warns when BUN_OPTIONS is set, because the one-shot commands still honour it.Requires Bun.
git clone https://github.com/alilibx/kerstel.git
cd kerstel
bun install
bun run --cwd packages/cli build
This produces a single compiled binary at dist/kerstel — no Node or Bun runtime required to run it.
The runtime hook must be built before anything runs from source — the CLI embeds packages/hook/dist/preload.cjs and worker.cjs at import time, so bun run packages/cli/src/index.ts fails on a fresh clone until bun run --cwd packages/hook build has run once; the build command above and bun run test below both do it for you.
bun run typecheck
bun run test
Contributions are welcome! Here's how:
git checkout -b feat/my-feature)CHANGELOG.md, and tick ROADMAP.md if you finished an item on itbun run test)git commit -m 'feat: add my feature')git push origin feat/my-feature)The macOS Keychain tests in packages/cli/test/keychain.test.ts write to and delete from the real login Keychain, so they are skipped unless you set KERSTEL_ALLOW_REAL_KEYCHAIN_TESTS=1.
Please keep PRs focused — one feature or fix per PR. AGENTS.md has the full contributor rules, including how to update ROADMAP.md and CHANGELOG.md. Coding agents read it too.
MIT — free to use, modify, and distribute.
kerstel.dev · Docs · Security model · Changelog · Roadmap
--preloadbunNODE_OPTIONS.gitignore if you said yes when asked. The question comes before "Apply these changes?" and defaults to no: if .gitignore currently hides your env files, the wizard asks whether it should remove those lines and add a one-line note instead, so the reference-only files can be committed. It checks the files as they will be written first: if any key still holds a plaintext value — --keep, a plaintext answer, or a line it could not parse — it names those keys (or the file and line, for a line with no readable key) rather than telling you the files are safe to commit. Decline and it leaves .gitignore untouched.KERSTEL_RELEASES_URL), made by ks update, ks --version (or ks version) on a terminal, and ks doctor; it carries nothing about your vault, and nothing about you beyond what any web request carries. Nothing else in Kerstel touches the network. Kerstel has no account, collects no telemetry, and listens on no network port. The security model page has the details.ks get --reveal, the daemon's token file, or the credential store directly. The 0700 home and 0600 files keep other users out, not you. Kerstel closes the leak where a secret sits in plaintext on disk and reaches git, a backup, or a tool that reads files; it does not stop code you ran from asking for it. See the design spec for the full threat model and the access-level protection planned on top of it.