Skip to main content

detssh

Deterministic Ed25519 SSH key generation from a seed passphrase. Same passphrase + same parameters → same keypair, every time, on any machine - nothing to back up but what's in your head.

detssh

Prompts for everything (passphrase hidden + confirmed) and writes to the same place ssh-keygen would: ~/.ssh/id_ed25519 / ~/.ssh/id_ed25519.pub (mode 600/700, ~/.ssh created if missing). Pass --output to write somewhere else instead.

Non-interactively:

detssh --kdf argon2id --salt-algo blake2b --seed "..." --label github --overwrite-files

Any flag switches to non-interactive mode: missing optional fields fall back to their defaults, --seed is the only required one. Passing --seed on the command line puts it in your shell history and process list (ps aux) - fine for local scripting, but prefer the interactive prompt when that matters.

By default the private key file is written unencrypted, like ssh-keygen -N "". Pass --key-passphrase (or leave it blank at the interactive prompt to skip) to encrypt it at rest instead - anyone who gets the file will still need that passphrase to use it. This is unrelated to the seed passphrase: it never touches key derivation, so it doesn't need to be remembered to recreate the key, only to unlock the file you already have. Same caveat as --seed applies if passed as a flag.

Only installing the public key

Because the same seed and parameters always give the same key, a machine you only want to log into doesn't need the private key at all. Run this on that machine instead:

detssh --authorize

It prompts for the same seed, label and backend settings as a normal run, then appends the public key to ~/.ssh/authorized_keys, ssh-copy-id style. No private key is written. If the key is already there, the file is left untouched, so running it twice is harmless. The interactive wizard asks about this up front too ("Only add the public key to an authorized_keys file").

In flag mode, add --authorize to the usual flags, plus --authorized-keys FILE for a file other than ~/.ssh/authorized_keys. --output, --key-passphrase, --register and --overwrite-files are rejected alongside it, because they only apply when a private key is written.

Using the key with ssh

ssh only auto-offers keys with default filenames (id_rsa, id_ed25519, ...) sitting directly in ~/.ssh/ - a key written under --output or a per-label subdirectory needs either -i <path>, a hand-written Host block in ~/.ssh/config, or ssh-adding it to your agent.

The interactive run offers to do this for you at the end, right after it writes the key:

Register this key so `ssh isik:personal:contabo` connects with it? [Y/n]: y
Destination (user@host): root@contabo.com
Non-default ssh port:

Registered isik:personal:contabo -> root@contabo.com
Run: ssh isik:personal:contabo

The label you derived the key with doubles as the Host name, so there's nothing extra to type. In flag mode, --register root@contabo.com (plus --register-port when it isn't 22) does the same thing, and needs --label for that reason. Declining changes nothing about the key that was written.

For a key you already have, detssh ssh register reaches the same end state:

detssh ssh register isik:personal:contabo root@contabo.com --key ~/.ssh/isik:personal:contabo/id_ed25519

Either route records the label in ~/.config/detssh/config, generates a Host block for it in a detssh-managed file, and adds a single Include line to ~/.ssh/config (once, at the top) that pulls it in. After that, ssh isik:personal:contabo just works - through real ssh, nothing routed through detssh at connect time.

detssh ssh list              # show registered labels
detssh ssh forget <label>    # remove a label (never deletes the key file)

Backends

  • --kdf: argon2id (default), argon2i, argon2d, scrypt, pbkdf2, bcrypt_pbkdf
  • --salt-algo: blake2b (default), blake2s, sha256, sha384, sha512, sha3_256, sha3_384, sha3_512

Each combination has its own options (cost knobs, salt digest size, etc.), with their valid ranges shown in both --help and the interactive prompts. See them with:

detssh --kdf <kdf> --salt-algo <salt-algo> --help

We recommend sticking with the defaults (argon2id, memory-hard with a high cost) unless you have a specific reason not to - they make brute-forcing your passphrase significantly more expensive than faster KDFs like pbkdf2.

Cost knobs (iterations, memory, cost, rounds, ...) also carry a soft safety limit meant to catch typos before they cost you minutes of derivation time. In interactive mode, going over one doesn't interrupt you - everything you exceeded is summarized in one confirmation at the very end. In flag mode, it fails immediately unless you pass --force-allow-soft-constraints.

Determinism guarantee

Same explicit parameters → same key, across any release sharing the same major version - only a major version bump may change key derivation. That guarantee starts at 1.0; we're still on 0.x, so it doesn't apply yet (see tests/golden/README.md).

Development

uv sync
uv run pytest
uv run ruff check .
uv run ruff format --check .

pytest runs with coverage on by default ([tool.pytest.ini_options] in pyproject.toml) and fails under 100% line+branch coverage - same gate locally and in CI, nothing extra to remember to pass.

100% coverage means every line runs under test, not that every behavior change gets caught - a test can execute a line without asserting anything meaningful about it. mutmut checks the second thing: it rewrites the source one small change at a time (flips a comparison, drops an argument, tweaks a string) and reruns the tests against each mutant, one at a time. A mutant that still passes ("survived") is a change the suite wouldn't notice.

uv run mutmut run       # takes a few minutes; ctrl-c-safe, resumes where it left off
uv run mutmut results   # list survivors by name
uv run mutmut show <name>   # see what a specific mutant changed

A survivor is either a real test gap (add an assertion that would fail against the mutant) or a genuinely equivalent mutant - the mutated code is provably indistinguishable from the original for every input (e.g. "utf-8" vs "UTF-8" as a codec name: Python's codec lookup is case-insensitive, so no test could tell them apart). CI (.github/workflows/tests.yml, job mutation) fails on any survivor not listed in tests/mutmut_known_equivalents.txt, which names each one and says why - add a confirmed-equivalent mutant there (never to silence a failure without doing that check) rather than chasing an unwinnable test.

Metadata

Release files for detssh 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for detssh 0.7.0
File Size Uploaded Provenance
detssh-0.7.0.tar.gz 125.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for detssh 0.7.0
File Interpreter ABI Platform
detssh-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 152.9 kB

Release files / detssh-0.7.0.tar.gz

Download URL detssh-0.7.0.tar.gz
Size 125.8 kB
Tags Source
SHA-256 checksum
How to use checksums
37bf0100842be2afae9638272480d3d66333df954266143a7ef4b5f1d76b3e95
BLAKE2b-256 checksum
How to use checksums
7da34184808a0e2389b208850bbc40d3c390d3e0aadeffec2c050bec4a9bed3b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / detssh-0.7.0-py3-none-any.whl

Download URL detssh-0.7.0-py3-none-any.whl
Size 27.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6534ae63884435641314cab79969671615e4fdd4ffe50d7a0d9503777193f6e0
BLAKE2b-256 checksum
How to use checksums
965106523df88237c3bc86cef293e441df076e200acb89e895f247ded856a564
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Security for detssh 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for detssh-0.7.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page