The list-and-arrow-keys interface I wrote was not the thing to build. What `seedpass` opens is a numbered menu tree, and navigating it is typing a number and pressing Enter, with a blank line to go back -- so an interface driven by arrow keys and single-letter bindings was unnavigable to anyone who already knows SeedPass, however well it worked on its own terms. This follows src/main.py item for item: the same eight main-menu entries in the same order, the same nine entry types under Add Entry, the same eighteen settings under the same numbers, the Profiles and Nostr submenus, the entry action and edit menus with Python's letter keys. Numbering is part of the interface, not decoration -- 7 then 1 has to still be Settings then Profiles -- so items this build cannot perform stay in place and say why when selected, rather than vanishing and shifting everything below them. Line-based means no raw mode, no alternate screen, and no terminal to restore, which removes a whole class of failure the previous version had to defend against. Raw mode is now entered only around a password prompt, where echo must be off. The agent-blind rule survives the change: listings and details show metadata with stored secrets as has_* flags, and a secret appears only through Show Secret or Copy. Secret Mode -- a setting Python already has -- routes both to the clipboard instead of the screen, so it means the same thing in both implementations. The key/value field, hidden custom fields, and seed phrases are read without echo. Three settings needed new profile operations: changePassword re-wraps only parent_seed.enc, since the index is keyed by the seed rather than the password and a half-applied change would leave the two disagreeing; writeParentSeed and setProfileName back the profile menu. Verified through a pty against a real vault -- every menu, adding a password entry, editing a label, revealing a secret -- and the derived values match the CLI. 28 menu tests drive the tree through a scripted UI, including that listings never contain a stored secret and that a non-numeric length is refused rather than stored as NaN. 128 CLI tests, 220 core tests, 36/36 cross-impl checks. test/nostr.test.ts is flaky under parallel load (it uses a live relay); it failed once during this work and passed on rerun and three times in isolation. Co-Authored-By: Claude Fable 5 <[email protected]>
seedpass-js
The SeedPass command-line interface, TypeScript port.
It reads and writes the same ~/.seedpass profiles as the Python
implementation — same derivations, same vault format, same Nostr sync — and
adds a session agent plus a scoped-token surface designed for AI agents and
automation.
Install
Requires Node.js 22 or newer.
# from a checkout
cd js && pnpm install && pnpm --filter @seedpass/cli build
node packages/cli/dist/seedpass-js.mjs --version
The build produces one self-contained file (dist/seedpass-js.mjs) plus a
.sha256 next to it. Copy it anywhere on your PATH; it has no runtime
dependencies beyond Node itself.
Quick start
export SEEDPASS_APP_DIR=~/.seedpass # omit to use the default
# Create a profile (mnemonic and password come from the environment,
# never from the command line)
export SEEDPASS_MNEMONIC="your twelve or twenty-four words"
export SEEDPASS_PASSWORD="your master password"
seedpass-js fingerprint add --name main
unset SEEDPASS_MNEMONIC # not needed again
# Unlock once per session through the agent
seedpass-js agent start &
seedpass-js vault unlock --ttl 900
seedpass-js entry add password github.com --length 20 --username you
seedpass-js entry reveal github.com
seedpass-js entry totp-codes
seedpass-js use github.com --clipboard
seedpass-js vault lock
Design: reference-first output
Default output never contains secret values. Entries are addressed by
reference (sp://entry/<id>), and secret-bearing fields are reported as
has_* flags:
$ seedpass-js entry get api-token
{ "id": "5", "ref": "sp://entry/5", "label": "api-token",
"kind": "key_value", "has_value": true, ... }
Plaintext leaves the vault only through commands that say so
(entry reveal, entry totp-codes, entry export-document,
util generate-password), or through a sink, which delivers a secret to its
destination without routing it through this process's output:
seedpass-js use api-token --exec ./deploy.sh # SEEDPASS_SECRET in the child env
seedpass-js use api-token --stdin-to "wc -c" # quote specs that contain flags
seedpass-js use api-token --clipboard
Scoped tokens for agents
An agent can be given a token that grants exactly what it needs and nothing else. Secrets are materialized inside the session agent, so the token holder never receives the seed and can never escalate to owner operations.
# As the owner:
seedpass-js agent token-issue --scope read use \
--kind key_value --label-regex '^api-' --uses 2 --ttl 600
# In the agent's process — no mnemonic, no password:
export SEEDPASS_TOKEN=<token>
seedpass-js entry list # metadata only
seedpass-js use api-token --exec ./deploy.sh # permitted
seedpass-js entry reveal api-token # denied: scope 'reveal' not granted
# Back as the owner — every grant, delivery and denial is recorded:
seedpass-js agent audit-verify
seedpass-js agent audit-tail -n 20
Environment
| Variable | Purpose |
|---|---|
SEEDPASS_APP_DIR |
Profile directory (default ~/.seedpass) |
SEEDPASS_MNEMONIC |
Parent seed; needed for profile creation, otherwise use the agent |
SEEDPASS_PASSWORD |
Master password, for fingerprint add and vault unlock |
SEEDPASS_TOKEN |
Scoped token; puts the CLI in token mode |
SEEDPASS_AGENT_SOCK |
Override the agent socket path |
SEEDPASS_SECRET |
Set by use --exec in the child process only |
Compatibility
Interoperability with the Python implementation is verified on every change
by scripts/cross_impl_check.py: each side opens the other's profiles,
backups round-trip, edits are mutually visible, conflict merges agree, and a
snapshot published by Python restores here with secrets intact. See
docs/typescript_port_compatibility_matrix.md.
Not yet ported: PGP RSA keys (not byte-reproducible; ed25519 is at parity),
and the semantic and api command groups.