npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chrisrivera/envsync

v3.0.2

Published

Sync a repo's gitignored .env files across your machines, end-to-end encrypted under a key chris-rivera.dev has never seen.

Downloads

2,013

Readme

@chrisrivera/envsync

Sync a repo's gitignored .env files across your machines, end-to-end encrypted under a key chris-rivera.dev has never seen.

git clone https://github.com/you/your-repo && cd your-repo
bunx @chrisrivera/envsync

That is the whole thing. The first run signs you in through your browser and asks once for your recovery key; every run after that on that machine prompts for nothing.

What it syncs

Exactly the .env* files under the project root that git ignores and does not track. Computed, never configured — so a committed .env.example, or an encrypted .env some other tool checked in, is excluded by construction rather than by a rule you have to remember to write.

A file git tracks is excluded and named in the output, never carried. Git already syncs it, and a second source of truth for the same bytes is a divergence waiting to happen.

What the server can see

Ciphertext, and metadata: project slugs, file counts, byte sizes, timestamps, device labels. Not filenames, and not a single value. The vault key is 256 CSPRNG bits that exist only on your machines and in your password manager; chris-rivera.dev stores an HMAC of a constant under it, which verifies a key it can never guess.

There is no reset. Lose the recovery key with no machine still holding vault.key and the data is gone — by construction, not by policy. Run envsync export <dir> onto an encrypted drive the day you set this up.

It never prints a secret

There is no get, no --stdout, no run -- and no --show-values. envsync status and --dry-run print paths, byte counts and key names. The job is to put a 0600 file on disk and stop.

The vault

envsync push creates the vault if the account has none: it is the machine with the files, it cannot seal them without a key, and it already chains login the same way. Registering the verifier is a write, so this needs the write-scoped credential only push carries. The ceremony is not shortened by being chained — the recovery key is still shown once and still has to be acknowledged — and without a terminal it refuses rather than printing a recovery key into a log. envsync vault init is still there when you want the step to be explicit.

envsync pull never creates one. A vault that does not exist has no versions, so creating one mid-pull would hand you an empty vault and nothing to pull — and it would generate the one unrecoverable secret in this tool as a side effect of a read, on the machine least likely to be the one that should hold it.

Pulling onto a machine that already has a .env

pull never overwrites. A file that differs from the vault is left exactly as it is, and the vault's copy is written beside it:

.env.local            1.9 kB   sidecar    gitignored
                               → .env.local.envsync, yours left as it is

Merge what you want out of it and delete it. The two versions of a .env are rarely one right and one wrong — one machine has the key you added this morning, the other has the one somebody rotated last week — so the merge belongs to the person who knows which is which.

--force overwrites instead (the previous contents go to backups/ first), and --keep writes nothing at all. Sidecars are gitignored or they are not written: add *.envsync to .gitignore, since the .env*.local pattern that protects .env.local does not cover .env.local.envsync.

Commands

envsync                       pull, acquiring whatever it needs on the way
envsync pull    [--project <p>] [--force] [--keep] [--dry-run] [--version <n>]
envsync push    [--project <p>] [-m <note>] [--yes] [--dry-run]
envsync status
envsync login   [--device] [--no-device] [--write] [--label <n>] [--timeout <s>]
envsync logout  [--all]
envsync init    [--name <slug>] [--project <id>]
envsync link    <projectId>   |   envsync unlink
envsync projects [ls | archive <slug>]
envsync history [--limit 20]
envsync devices [ls | revoke <id> | revoke --all-others]
envsync vault   [init | unlock | rotate | id]
envsync export  <dir> [--yes]
envsync doctor

Global flags: --json --quiet --no-color --host <url> --config-dir <path>.

Environment: ENVSYNC_TOKEN ENVSYNC_VAULT_KEY ENVSYNC_PROJECT ENVSYNC_HOST ENVSYNC_CONFIG_DIR ENVSYNC_NO_BROWSER NO_COLOR.

Exit codes

| Code | Meaning | | ---- | ---------------------------------------------------------------------------- | | 0 | ok | | 1 | refused, or a file was deliberately skipped | | 2 | usage, or no project resolves here | | 3 | authentication required and nobody could be asked | | 4 | forbidden: token scope, or this deployment's owner gate | | 5 | network, or the server failed | | 6 | refused for safety: not gitignored, sidecar not gitignored, version conflict | | 7 | the vault key is missing, wrong, or opens nothing |

With --json, stdout carries one object and nothing else — failures included:

{
  "ok": false,
  "code": 3,
  "error": "auth_required",
  "hint": "run `envsync login`"
}

Everything a human reads goes to stderr, in every mode, so envsync status --json | jq works inside a script.

Credential storage

| Platform | Directory | | -------- | ---------------------------------------- | | Windows | %APPDATA%\envsync\ | | macOS | ~/Library/Application Support/envsync/ | | other | ${XDG_CONFIG_HOME:-~/.config}/envsync/ |

auth.json, vault.key, config.json, backups/. Directory 0700, files 0600.

Plain files, not the OS keychain, and envsync status says so in those words. Windows Credential Manager caps a credential blob at 2560 bytes, a keyring binding means a compiled native dependency (which breaks the zero-install bunx promise), and a silent fallback to plaintext is worse than an honest plaintext file — because people believe the marketing. On Windows the real protection is an icacls pass, and envsync doctor reports whether it took.

Requirements

Node 22 or newer. Zero runtime dependencies: node: builtins and fetch.

The floor was Node 20 until it reached end of life on 2026-04-30. Every release runs the built bundle on the floor before publishing, so the number above is tested rather than claimed.

Staying current

bunx @chrisrivera/envsync does not necessarily run the newest release. bunx keeps a per-package project under your temp directory with its own lockfile, named for the spec rather than the version:

$TMPDIR/bunx-<uid>-@chrisrivera/envsync@latest/bun.lock

Once that lockfile pins a version, the bare command reuses it until bun's cached registry data expires. Adding @latest changes nothing, because envsync@latest is the spec already locked.

So every run checks npm for a newer release and prints a line on stderr if you are behind, with an exact version in the command it suggests:

envsync <newest> is out. You are running <yours>.
  bunx @chrisrivera/envsync@<newest>
  The version is pinned on purpose: bunx caches the unpinned command.

A pinned spec gets its own directory and resolves fresh, which is why the notice never tells you to re-run the command you just ran.

To pin without reading the notice first, resolve the version and hand bunx an exact spec in one command — this repo's syncenv:check script does exactly that:

bunx @chrisrivera/envsync@$(npm view @chrisrivera/envsync version) check

The check writes nothing, stores nothing, and gives up after 1.5 seconds. It runs alongside your command rather than in front of it, so a working network costs you nothing and a broken one costs you the timeout, once, after the work is already done. It is silent under --json and --quiet, and a registry that is unreachable, slow, or behind a captive portal is silence rather than an error. Nothing it does can fail your command.

Pinning

The next thing you type into this CLI is your recovery key, so pin the version if running whatever npm currently serves bothers you:

bunx @chrisrivera/envsync@<version>

envsync --version prints the one you are on, and the npm page lists what has shipped. A real number is deliberately not written here: this file is inside the published tarball, so any version named in it is the version you already have, and it goes stale the moment the next release lands.

The CLI is never the only copy of your keys. That is what envsync export is for.

License

MIT