CLI
Install or run once
npx @agntn/hashes sha256 "hello world"
pnpm add -g @agntn/hashes # then plain `hashes`
Commands
| Command | What it does | Example |
|---|---|---|
hash | Digest of text, or of stdin with - | hashes hash blake3 - < file.bin |
hmac | Keyed digest | hashes hmac sha256 message secret |
verify | Compare with an expected digest, exit 1 if not | hashes verify md5 hello 5d41402abc4b2a76b9719d911017c592 |
extend | Length extension, a digest and a message per secret length | hashes extend md5 <digest> --message user=guest --suffix x --secret-length 3 |
identify | Candidates for a hash, by prefix or length | hashes identify '$2b$10$...' |
search | The words and the recipe behind a digest, exit 1 if none | hashes search <digest> alpha beta --rounds 2 |
algorithms, info | The list, -f keeps one family, -c one category. Name one and you get its options | hashes info pbkdf2 |
mcp | The MCP server on stdio | hashes mcp |
hash is the default, so the first word can be the algorithm. hashes md5 hello and hashes hash md5 hello are the same call. No words at all? You get the list.
Every command comes from the same tool definitions hashes mcp serves, so its flags are the schema's properties and its help is the schema's descriptions. extend and algorithms print the very text a model reads. The rest are the CLI's own. Why? A shell wants raw bytes in and out, the digest alone and a search that runs for minutes. A model wants none of that. Want data instead of text? Every command but hash, hmac and mcp takes --json:
hashes extend md5 <digest> --message m --suffix x --secret-length 3 --json | jq -r '.extensions[0].message'
Stdout is the digest
Only the digest goes to stdout, so it pipes and it lands in a variable without trimming:
digest=$(hashes sha256 abc)
echo -n abc | hashes sha256 -
# ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
A KDF's salt and cost go to stderr, since the digest is useless without them. -e binary writes the raw bytes, for xxd or the next tool in the pipe. Stdin is read byte for byte, so a file hashes the same as sha256sum would hash it.
Several at once
One input gets its digest alone. Two or more get sha256sum lines, the same ones hashes_compute hands a model:
hashes sha256 "hello world" foo
# 2 inputs, sha256, hex, 32 bytes
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 "hello world"
# 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae "foo"
The heading goes to stderr and the lines to stdout. Your quotes count: "hello world" is one input, not two. Each input is printed in JSON quotes. A line feed inside one can't fake a line of its own.
hmac and verify want the key or the expected digest last, the way cp wants its target. So hashes hmac sha256 message secret means what it always did. More texts just go before the key:
hashes verify md5 nope hello world 5d41402abc4b2a76b9719d911017c592
# MATCH: 1 of 3 inputs gives the expected md5 digest
# expected 5d41402abc4b2a76b9719d911017c592
# mismatch 4101bef8794fed986e95dfb54850c68b "nope"
# match 5d41402abc4b2a76b9719d911017c592 "hello"
# mismatch 7d793037a0760186574b0282f2f435e7 "world"
Which guess was the password? Now you know. It exits 1 only when none of them is.
identify takes several hashes and prints a block for each. A KDF draws a salt per input and puts it on that input's line. - is still stdin, once per call, because stdin doesn't refill. And -e binary wants a single input. Raw digests in a row have nothing between them.
Flags
hashes hash --help
-e, --encoding picks the output: hex (default), base64, base64url, binary. --input-encoding picks how the input is read: utf8 (default), or hex and base64 for the bytes they spell. --key-encoding does the same for the HMAC key.
hashes hash160 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 --input-encoding hex
# 751e76e8199196d454941c45d1b3a323f1433bd6
Without the flag that same line hashes 66 characters of text, which is a valid digest of the wrong thing. The flag also applies to stdin.
The rest of the flags come from what the algorithms declare in info().options: --salt, --n, --r, --p, --key-length, --iterations, --digest for the KDFs, --seed for xxHash, --passes and --bits for HAVAL, --rounds and --chain for every fixed-length digest. Of these, hmac takes only --passes and --bits. HMAC runs one round. Each one is the option's name in kebab case, so keyLength turns into --key-length and scrypt's capital N into a humble --n. The help text names the algorithms that take each one, or the few that don't. --bits means one thing to HAVAL and another to xxHash. The help gives both. A flag the algorithm doesn't declare is an error, not a digest computed without it:
hashes xxhash abc --seed 1
# bea9ca8199328908
hashes sha256 abc --rounds 2 --chain hex
# dfe7a23fefeea519e9bbfdd1a6be94c4b2e4529dd6b7cbea83f9959c2621b13c
hashes sha256 abc --seed 1
# Invalid option seed=1: sha256 takes rounds, chain
A flag no algorithm has is refused before anything gets hashed, with the whole list of what the command takes. Typos don't sneak through:
hashes sha256 abc --sead 1
# Invalid arguments: unknown option "--sead"; takes --input-encoding, --encoding (-e), --rounds, --chain, ...
Exit codes
0 for a digest, and for verify a match. 1 for a mismatch and for an identify or search that comes back empty. A list follows suit: 1 when one input fails, when no candidate matches, or when one hash fits nothing. Also 1 for any error in the input: an unknown name, a bad option, a missing salt. The error is one line on stderr, with no stack trace:
hashes sha265 abc
# Unknown algorithm: sha265. Available: sha256, sha384, sha512, ...
hashes CRC-32/BZIP2 abc
# Unknown algorithm: CRC-32/BZIP2. Did you mean crc32 with variant=bzip2?
So it's hashes crc32 abc --variant bzip2, and out comes 648cbb73. Right name, one flag away.
A pipe that closes early (| head -1) ends the process quietly instead of throwing EPIPE at you. There are no colours at all, so a pipe, a file or an agent's transcript never gets escape codes.