CLI
A codec-first CLI, no install required. The codec is the first token and is
never inferred — every command is ids <codec> <verb>. Run npx @smonn/ids --help
for the full list, or npx @smonn/ids <codec> --help for one codec’s verbs.
ids <codec> <verb> [args] [flags]ids keygen [--bytes 16|24|32] [--key-encoding hex|base64url]ids convert <brand> --uuid <uuid>ids --version | --helpThe full contract lives in the repo’s CLI specification; the rationale is in ADR-0032 (grammar) and ADR-0033 (key model).
Codec / verb matrix
Section titled “Codec / verb matrix”| codec | write verb | read verb | needs key | write input |
|---|---|---|---|---|
timestamp |
generate |
inspect |
no | brand only |
reverse |
generate |
inspect |
no | brand only |
signed |
generate |
inspect |
yes | brand only |
opaque |
generate |
inspect |
yes | brand only |
wrapped |
wrap |
inspect |
yes | brand + integer + kind |
digest |
derive |
match |
yes | brand + material + ns |
Write verbs
Section titled “Write verbs”Write verbs name their input, because it differs per codec. All write output is the bare ID(s), one per line — directly pipeable.
# Timestamp / Reverse (no key)$ npx @smonn/ids timestamp generate usr --count 3usr_…usr_…usr_…
# Backfill at an explicit creation time (ISO 8601 or epoch-ms, interpreted UTC)$ npx @smonn/ids timestamp generate usr --at 2026-06-01T00:00:00Z
# Signed / Opaque (keyed)$ npx @smonn/ids signed generate usr --key-file ./key.hex
# Wrapped — wrap an integer; --kind is required (ranges overlap, so it can't be inferred)$ npx @smonn/ids wrapped wrap ord --value 18446744073709551615 --kind u64 --key-file ./key.hex
# Digest — derive a stable ID from material; material via --material or stdin (stdin keeps PII off argv)$ printf '%s' "user@example.com" | npx @smonn/ids digest derive psd --ns billing --key-file ./key.hex--count N(-calias,generateonly, default 1, max 10000): mint N independent IDs.--at WHEN(generateonly): ISO 8601 datetime or integer epoch-ms, interpreted as UTC. With--count, all share the timestamp but keep distinct random tails.--value/--kind(wrap): the integer and its width/signedness (u32/i32/u64/i64).--valueis parsed as a string then range-checked.--ns/--material(derive): the namespace (required, non-empty) and the material (via--material, or stdin when absent).
Read verbs
Section titled “Read verbs”inspect reads an ID and reports what the codec can recover; match (digest only)
recomputes the digest and compares. Human output is aligned key: value lines;
--json switches to a machine object, --quiet silences stdout (exit code remains
the signal).
$ npx @smonn/ids timestamp inspect usr_06f80z92d2dbsqqg28t5cy4tqgbrand: usrcodec: timestamptimestamp: 1780272145000 (2026-06-01T00:02:25.000Z)uuid: 019e807d-2268-9abc-def0-123456789abc
$ npx @smonn/ids wrapped inspect ord_… --key-file ./key.hex --json{"brand":"ord","codec":"wrapped","value":"18446744073709551615","kind":"u64","uuid":"…"}Per codec:
| codec | operation | fields reported |
|---|---|---|
timestamp |
decode timestamp | brand, codec, timestamp, uuid |
reverse |
decode timestamp | brand, codec, timestamp, uuid |
signed |
verify signature, then decode ts | brand, codec, timestamp, verified, uuid |
opaque |
decrypt, then decode timestamp | brand, codec, timestamp, uuid |
wrapped |
unwrap to the original integer | brand, codec, value, kind, uuid |
Notes:
- Every
inspectreports auuidfield. The reverse direction (uuid → id) is the top-levelconvertcommand. signed inspectcouples verification with extraction: it requires the key, and only on a verified signature does it emit the report (verified: true) with exit 0. A failed signature, missing/invalid key, or malformed ID is a failure — a stderr diagnostic, a non-zero exit, and no stdout report (there is noverified: false).opaque inspectreports whatever the (unauthenticated) codec decrypts: a wrong key yields a plausible-but-wrong timestamp, not an error — Opaque and plaintext Timestamp IDs are wire-indistinguishable.wrapped inspectis self-describing: the kind is recovered by trial (each ofu32/i32/u64/i64is verified against the tag), so no--kindis needed on read — though an optional--kindskips the trial. Au64/i64value is emitted in JSON as a string to avoid precision loss above 2^53.
Batch input (stdin)
Section titled “Batch input (stdin)”inspect reads many IDs from stdin (one per line) in addition to a single positional —
best-effort: stdout carries only successes (--json ⇒ NDJSON), stderr a per-line
diagnostic, and the exit code is 0 only if every line succeeded.
grep -o 'usr_[0-9a-z]*' app.log | npx @smonn/ids opaque inspect --key-file ./key.hex --jsonmatch is single-shot (one ID per invocation) with a grep-like exit: 0 matched,
1 no match, 2 error.
$ printf '%s' "user@example.com" | npx @smonn/ids digest match psd_… --ns billing --key-file ./key.hexmatch: trueTop-level commands
Section titled “Top-level commands”These are codec-agnostic, so they sit outside the codec tree.
keygen
Section titled “keygen”Emit fresh random key material for any keyed codec. A secret — do not log or commit.
One key backs every keyed codec (the library derives per-codec subkeys internally). A
reminder is printed to stderr, so export IDS_KEY=$(npx @smonn/ids keygen) and pipes are
unaffected.
$ npx @smonn/ids keygena1b2c3… # 32 bytes, hex
$ npx @smonn/ids keygen --bytes 16 --key-encoding base64urlAbCdEf…--bytes 16|24|32(default 32). Shorter keys only lower the entropy floor — the library always derives AES-256 via HKDF (per ADR-0027), so--bytes 16does not yield AES-128.--key-encoding hex|base64url(defaulthex, orIDS_KEY_ENCODING).
convert
Section titled “convert”Re-express a UUID as an Id for a brand (uuid → id). Codec-agnostic — the mapping is a
view over the shared payload. The reverse direction (id → uuid) is the uuid field of
inspect.
$ npx @smonn/ids convert usr --uuid 0190ab12-3456-789a-bcde-f0123456789ausr_…Keyed commands resolve one key. It is a bare encoded blob — there is no format prefix or codec tag, and no per-codec key env vars (the codec is the command token).
Value — first present wins: --key STRING › --key-file PATH › IDS_KEY. Supplying
both --key and --key-file is a usage error. Prefer --key-file/IDS_KEY over --key,
which is visible in ps and shell history. Using --key emits a one-line advisory on
stderr; IDS_KEY is silent.
Key-file permissions — when --key-file is used the CLI checks the file’s permission
bits after reading it. If the file is accessible to group/others (mode bits 0o077 set), it
emits a one-line warning on stderr recommending chmod 0600 '<path>'. The check skips
silently where stat is unavailable or mode bits are meaningless (e.g. Windows).
Encoding — independent of the value source: --key-encoding hex|base64url ›
IDS_KEY_ENCODING › hex. So a base64url key can be fully env-configured:
export IDS_KEY_ENCODING=base64urlexport IDS_KEY=$(npx @smonn/ids keygen --bytes 32)npx @smonn/ids signed generate usrThe decoded key must be 16, 24, or 32 bytes; any other length is a usage error (this catches truncated or wrong-encoding pastes). A malformed encoding (bad hex characters, non-canonical base64url padding bits) is also a usage error (exit 2).
Digest material — derive and match accept their input material via --material <value> or stdin. Using --material emits a one-line advisory on stderr because the value
(often PII such as an email address) would appear in ps and shell history; piping via
stdin keeps it off argv. Machine output (stdout/JSON) is unaffected by the advisory.
Stray positional errors — if an unrecognised positional token is passed (for example a
mistyped bare key), the CLI truncates it to 20 characters followed by … in the error
message so the full token does not appear in stderr or CI logs.
Error-echo hygiene — flag values and file paths in error messages are stripped of
control characters, Unicode bidi/format controls, and line/paragraph separators (U+2028–U+2029)
before echo. Non-path flag values (e.g.
--count, --kind, --key-encoding, --bytes, --at invalid dates) are additionally
truncated to 20 Unicode code points. File path diagnostics (e.g. --key-file errors and
warnings) are stripped but not truncated, so the full path remains readable.
Exit codes
Section titled “Exit codes”| code | meaning |
|---|---|
0 |
success (all lines succeeded in a batch) |
1 |
operational failure (malformed ID, failed verify/decrypt, wrong key); for match, “no match” |
2 |
usage error (unknown/missing/conflicting flag, bad brand, unsupported key length, out-of-range value) |