Summary
Identity portability across machines and CI: hulak env export-key, hulak env import-key, plus a friendly error when HULAK_MASTER_KEY doesn't match.
Goal
Users need a way to back up their age private key and restore it on another machine. They also need a clear error when the wrong key is provided in CI.
The key lives at ~/.config/hulak/identity.txt (file mode 0600). These commands read and write that file safely.
Commands
hulak env export-key — prints the private key to stdout with a one-line warning that this is the master key
hulak env export-key --out <path> — writes the key to a file with mode 0600 instead of stdout
hulak env import-key <path> — reads a key from a file and writes it to the identity location
hulak env import-key --stdin — reads a key from standard input (for piping out of a password manager)
hulak env import-key ... --force — overwrites an existing identity file (default: refuse)
Cases to cover
- Export with no identity file → friendly error pointing to
hulak init
- Export to stdout always prints the "treat like a password" warning, even in non-TTY mode (don't suppress it)
- Export to
--out writes mode 0600 regardless of the umask
- Import validates the key with
age.ParseX25519Identity before writing — malformed input never lands on disk
- Import refuses to overwrite an existing identity unless
--force is passed
- Import normalizes whitespace (strips trailing newlines, ignores leading whitespace)
- Import sets file mode
0600 on the destination regardless of source file permissions
--stdin can read multiple lines but only uses the first non-empty, non-comment line as the key
HULAK_MASTER_KEY error UX (no command, just a wrapper)
When HULAK_MASTER_KEY is set and decrypt fails with "no identity matched any of the recipients", wrap it in a friendlier message:
- Wraps the raw age error so users don't have to know age internals
- Suggests common causes: the key is for a different project, you were removed as a recipient, or stray whitespace from a copy/paste
- Wraps
age.ParseX25519Identity parse errors with a hint to use the AGE-SECRET-KEY-1... format
This applies wherever the identity is loaded — hulak run, hulak env *, etc.
Cases to cover (HULAK_MASTER_KEY UX)
- Key set but doesn't match any recipient → friendly error, not raw age string
- Key has stray whitespace/quotes → friendly error suggesting verbatim paste
- Key in a clearly wrong format (e.g.,
age1... public key by mistake) → "this looks like a public key, not a private key"
- Key empty → "HULAK_MASTER_KEY is set but empty"
- No HULAK_MASTER_KEY and no identity file → original "no identity found" error stays as is
Depends on
Blocks
Nothing.
Additional cases to cover (review pass)
- Atomicity on import: write to
identity.txt.tmp then rename, so an interrupted write can never corrupt an existing identity file. Validation runs first; only well-formed input ever reaches the rename.
--out <path> collision policy: refuse if the destination file already exists; require --force to overwrite. Default is non-destructive. Mirror this with import-key.
--force scope: applies only to the "destination already exists" guard. Never bypasses key parsing/validation — malformed input must still be rejected even with --force.
- Identity precedence on import: if
HULAK_MASTER_KEY is set in the environment, import-key should refuse with a clear error. Importing while a master-key env var shadows the on-disk identity makes the import dead-on-arrival.
- Export works outside a project: identity is global, not project-scoped.
hulak env export-key from any directory still prints the key with the standard warning. Don't gate on project root detection.
- HULAK_MASTER_KEY is a valid age key for a different project (parses fine, just doesn't decrypt this store): same friendly "no identity matched any of the recipients" wrapping, but mention "this key may be for a different project, or you were removed as a recipient."
Summary
Identity portability across machines and CI:
hulak env export-key,hulak env import-key, plus a friendly error whenHULAK_MASTER_KEYdoesn't match.Goal
Users need a way to back up their age private key and restore it on another machine. They also need a clear error when the wrong key is provided in CI.
The key lives at
~/.config/hulak/identity.txt(file mode0600). These commands read and write that file safely.Commands
hulak env export-key— prints the private key to stdout with a one-line warning that this is the master keyhulak env export-key --out <path>— writes the key to a file with mode0600instead of stdouthulak env import-key <path>— reads a key from a file and writes it to the identity locationhulak env import-key --stdin— reads a key from standard input (for piping out of a password manager)hulak env import-key ... --force— overwrites an existing identity file (default: refuse)Cases to cover
hulak init--outwrites mode0600regardless of the umaskage.ParseX25519Identitybefore writing — malformed input never lands on disk--forceis passed0600on the destination regardless of source file permissions--stdincan read multiple lines but only uses the first non-empty, non-comment line as the keyHULAK_MASTER_KEYerror UX (no command, just a wrapper)When
HULAK_MASTER_KEYis set and decrypt fails with "no identity matched any of the recipients", wrap it in a friendlier message:age.ParseX25519Identityparse errors with a hint to use theAGE-SECRET-KEY-1...formatThis applies wherever the identity is loaded —
hulak run,hulak env *, etc.Cases to cover (HULAK_MASTER_KEY UX)
age1...public key by mistake) → "this looks like a public key, not a private key"Depends on
hulak env)Blocks
Nothing.
Additional cases to cover (review pass)
identity.txt.tmpthen rename, so an interrupted write can never corrupt an existing identity file. Validation runs first; only well-formed input ever reaches the rename.--out <path>collision policy: refuse if the destination file already exists; require--forceto overwrite. Default is non-destructive. Mirror this withimport-key.--forcescope: applies only to the "destination already exists" guard. Never bypasses key parsing/validation — malformed input must still be rejected even with--force.HULAK_MASTER_KEYis set in the environment,import-keyshould refuse with a clear error. Importing while a master-key env var shadows the on-disk identity makes the import dead-on-arrival.hulak env export-keyfrom any directory still prints the key with the standard warning. Don't gate on project root detection.