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

@phnx-labs/secrets-cli

v0.3.3

Published

Portable secret bundles, native stores and provider-backed injection

Downloads

2,686

Readme

secrets

Named bundles of environment variables, stored in the OS keychain, an encrypted file, or an age-encrypted vault — and injected into a child process without ever printing them.

npm i -g @phnx-labs/secrets-cli

secrets create prod --description "Production keys for the api stack"
secrets add prod STRIPE_API_KEY                 # prompts; value never echoes
secrets add prod LOG_LEVEL --value info         # a non-sensitive literal
secrets exec prod -- ./deploy.sh                # values ride the child env only

secrets runs on its own. It has no fleet registry and no dependency on agents-cli. Other programs drive it through a bounded private pipe (secrets __serve) or the SDK (@phnx-labs/secrets-cli).

Reading a named bundle from another program

@phnx-labs/secrets-cli/client is the process client for that pipe. Each call runs the installed secrets __serve ($SECRETS_BIN, else secrets on PATH) and exchanges one request over private fd 3 / fd 4 pipes, so the engine's access policy, grants and expiry checks apply and values never touch argv or stdout. It imports no provider SDKs.

import { readAndResolveBundleEnvSync, isSecretsClientError } from '@phnx-labs/secrets-cli/client';

const { env } = readAndResolveBundleEnvSync(
  'r2.backups',
  { caller: 'session-transport', agentOnly: true },
  { allowedBundles: ['r2.backups'] },
);

readAndResolveBundleEnv is the async form, and secretsRequest / secretsRequestSync send any other protocol operation. Failures are SecretsClientError with a code: policy and data codes such as ACCESS_DENIED, NOT_FOUND and EXPIRED are authoritative, and only the codes isSecretsTransportError accepts (missing binary, timeout, bad response) mean the engine could not be reached. An explicit SECRETS_HOME is passed through and an unset one stays unset. AGENTS_SECRETS_PASSPHRASE is accepted as an alias for SECRETS_PASSPHRASE. The client runs a JavaScript engine under node, also from a Bun caller, so Node must be on PATH there. The synchronous form runs the engine in its own process group and kills that group when the call ends, including on TIMEOUT. It is POSIX only and fails with SYNC_UNSUPPORTED on Windows.

Before its first data request the client sends the engine a handshake, once per process. An engine that reports a lower engine contract than the client's ENGINE_CONTRACT (every release up to 0.3.0) is refused with ENGINE_INCOMPATIBLE before any data operation reaches it, because it may read a different state root. This code is authoritative, not a transport error. checkSecretsEngine / checkSecretsEngineSync run the same check and return the handshake (protocol, contract, version). A push (pushBundleToHost and its siblings) checks its receiver the same way, through secrets __handshake over SSH under the same state root. A lower contract there returns { ok: false, code: 'ENGINE_INCOMPATIBLE' } and nothing is sent.

On macOS, secrets start (or the first unlock) spawns a user-local broker: secrets _agent-run. That is not the agents daemon. Socket and pid live at $SECRETS_HOME/.cache/helpers/secrets-agent/ (default ~/.agents/.secrets). Linux has no broker.

With agents-cli

agents setup secrets            # install if missing, then secrets migrate
agents secrets list             # passthrough to this binary; SECRETS_HOME=~/.agents

Where things live

Everything hangs off SECRETS_HOME (default ~/.agents/.secrets). When a harness swaps HOME for its own configuration and keeps the user's home in AGENTS_REAL_HOME (Cursor under agents), the default root and the legacy roots it adopts from resolve under AGENTS_REAL_HOME. HOME is not changed, and an explicit SECRETS_HOME still wins.

| Path | What | |---|---| | secrets.yaml | defaults (backend, policy, agent.*) and named provider connections | | .cache/secrets/ | the AES-256-GCM encrypted file store | | .secrets-key/ | the 0600 machine-local key for that store | | vault.age | the age-encrypted synced vault | | .cache/helpers/secrets-agent/ | the macOS broker's socket, pid and token | | .cache/events.jsonl | the value-free event log (secrets activity) |

The first time a command finds no store there, it adopts the stores of the earlier roots ~/.agents and ~/.secrets, re-encrypting each under the new root's key and leaving the old directories in place. A bundle that differs between them stops every command until secrets migrate --prefer <root> picks a copy; secrets migrate --dry-run shows the plan. macOS Keychain items are keyed by service name, not by path, so they are unaffected.

Backends

  • keychain (macOS) — items live in the login keychain behind Touch ID. The broker (secrets unlock, secrets status) holds an unlocked bundle in memory for the hold window (7 days; secrets policy --hold 8h changes it) so concurrent readers do not each prompt. One unlock, of one bundle or several, asks for Touch ID once. A locked bundle read from your GUI session — by you or by an agent you are driving — raises one Touch ID sheet and is then held; only an unattended run (AGENTS_RUNTIME=headless) or a process outside the GUI session (SSH, launchd, CI) refuses and asks for secrets unlock. Holds survive a broker restart or upgrade; secrets unlock --durable (or secrets.agent.durable: true) makes them survive sleep too. secrets unlock <bundle> --scope <scope> narrows that grant to one opaque scope (a harness name, a job id); a reader passes the same scope (SECRETS_SCOPE) to see it. Without --scope the grant is global.
  • file — AES-256-GCM under a machine-local key; readable headlessly.
  • vault — age-encrypted file you sync yourself (secrets vault unlock).

Commands

secrets list [query]             secrets view <name>          secrets activity [name]
secrets create [name]            secrets add <bundle> <key>   secrets rotate <bundle> <key>
secrets remove <bundle> <key>    secrets rename <old> <new>   secrets delete <name>
secrets describe <name> [text]   secrets policy [bundle] [p]  secrets generate [length]
secrets exec <bundle> -- <cmd>   secrets get <item> [key]     secrets set <item>
secrets import [bundle] …        secrets export [bundle] …    secrets migrate
secrets unlock [names…]          secrets lock [names…]        secrets status
secrets start | stop             secrets vault unlock | lock  secrets push | pull | remote-list
secrets import-keyring           secrets migrate-acl          secrets rekey | rotate-passphrase
secrets hosts pin | list     secrets mcp

secrets <command> --help prints an ordered example sequence before the flags.

secrets status --json reports the broker and held bundles without values: {"broker":{"running":true},"holdMs":604800000,"held":[{"name","keys","expiresAt","durable","survives"}]}. See docs/secrets.md for the fields.

scripts/generate-cli-docs.sh                              # refresh docs/
scripts/generate-cli-docs.sh --check                      # fail on stale docs
scripts/generate-cli-docs.sh --out-dir .agents/scratch/cli-docs  # preview

Run after bun install --frozen-lockfile. The script works from any directory; --out-dir is relative to your current directory. Open command-reference.html from the output folder to browse the tree and search commands. CI uploads a secrets-cli-docs artifact on pull requests and pushes to main or release/**. It contains the HTML reference plus Markdown and JSON indexes. Tests and releases check the committed files; commit regenerated docs/ with command changes. Release checks also run with --skip-tests / --skip-build.

Every command's arguments, options, examples and notes: docs/command-reference.html (searchable, with the command tree in a sidebar) and docs/command-index.md. Both are generated from the registered command tree by scripts/generate-cli-docs.sh; scripts/test.sh fails when they are stale.

Remote hosts

--host <target> on exec, import, export, list, view and unlock takes the address grammar shared by every Phoenix CLI (ssh://[user@]host, or a bare OpenSSH alias / user@host, exactly as ssh would) — secrets speaks the ssh scheme only, so vnc://, cdp:// and any other scheme fail loud, and so does a port on the address (put it in ~/.ssh/config instead). exec --host resolves the bundle on that host and runs the child locally; to run the child remotely, put secrets exec on the other side of ssh. Credential transfer refuses a host whose key is not pinned (secrets hosts pin <target>).

list and view also take --hosts a,b. With one host, --json prints that host's own JSON unchanged. With several, it prints one document, {"hosts":[{"host":"a","ok":true,"result":…},{"host":"b","ok":false,"error":"…"}]}, where result is what --host a --json would print and error carries only the ssh or exit reason and the remote's stderr. Any failed host makes the command exit 1. With --reveal the session runs on a PTY, which merges the remote's stderr into stdout, so a failed host's error is only the exit reason.

Development

bun install
bash scripts/test.sh            # vitest, real stores and processes, no mocks
scripts/generate-cli-docs.sh    # regenerate docs/command-* after a help change
bash scripts/build.sh           # tsc -> dist/
bash scripts/install.sh         # side-by-side dev install as `secrets-dev`
bash scripts/release.sh <ver>   # dry run; --apply publishes and smoke-installs

Release

Push a branch named release/x.y.z or release/x.y.z-pre.n (pre-tag optional). GitHub Actions tests, packs a tarball that must not contain src/ or .ts sources, publishes @phnx-labs/secrets-cli to npm (latest, or next for -pre.n), and tags v<version>.

The workflow publishes through npm trusted publishing: no npm token is stored in the repository. The package owner sets it up once, with a 2FA login: npm trust github @phnx-labs/secrets-cli --repo phnx-labs/secrets-cli --file release.yml (or npmjs.com → the package → Settings → Trusted publisher). See docs/RELEASE.md.

The npm package ships compiled dist/**/*.js and dist/**/*.d.ts — no TypeScript sources, no source maps, no src/. Before publishing, scripts/verify-client-package.sh installs the tarball into a clean prefix and reads a synthetic bundle through the public client and that install's real secrets.

See AGENTS.md for the contract this repository holds itself to.