Guide

CLI

The ciphers command and its subcommands. Then the flags each cipher reads.

Run it

shell
pnpm add -g @agntn/ciphers
ciphers caesar "ATTACK AT DAWN" --shift 3
# DWWDFN DW GDZQ

First word not a command? Then it's a cipher name and the CLI encodes. ciphers caesar ... and ciphers encode caesar ... are the same call, because typing encode every time gets old fast. No words at all lists the ciphers.

Every command is one of the agent tools. runCli from @agntn/tools builds them from the same schemas MCP, Pi and OMP read. So a flag the schema refuses gets refused here too. Same rule, your spelling: the line names --max-period where a model would see /maxPeriod. No second parser to drift away from the first one.

CommandDescription
ciphers encode <cipher> <text>Encode with a cipher
ciphers decode <cipher> <text>Decode with a cipher
ciphers brute <text>All 25 Caesar shifts, best --lang fit first (en, pl, ja)
ciphers frequency <text>Letter histogram and index of coincidence
ciphers period <text>Vigenère key lengths by column IoC and Kasiski, the likely key for the top three
ciphers guess <text>Likely cipher families, each with its signal and the call to try next
ciphers probe <text> --key <passphrase>Padding hits of a Salted__ blob across digests, key lengths and iterations, next to the count chance gives
ciphers crib '["<hex>","<hex>"]' --crib <text>Slide a crib across ciphertexts under one reused XOR key, most readable places first
ciphers hidden <text>A message hidden by position, the likeliest readings first, or one place with --pick
ciphers recover <text> --cipher <cipher>Search the key of a Vigenère, Beaufort, variant Beaufort, substitution or columnar text, best first
ciphers info [cipher]Every cipher by category, or one cipher's family, options and keyspace. --category classical keeps one category
ciphers ciphersSame as info, the old name still works
ciphers mcpRun the MCP server over stdio

Examples

shell
ciphers encode caesar "ATTACK AT DAWN" --shift 3     # DWWDFN DW GDZQ
ciphers decode atbash "ZGGZXP ZG WZDM"                # ATTACK AT DAWN
ciphers encode vigenere "ATTACK AT DAWN" --key LEMON  # LXFOPV EF RNHR
ciphers encode alberti "ATTACK AT DAWN" --key ALBERTI --period 4
ciphers encode affine "AFFINE CIPHER" --a 5 --b 8     # IHHWVC SWFRCP
ciphers encode rail-fence "WEAREDISCOVEREDRUNATONCE" --rails 3
ciphers encode playfair "HIDE THE GOLD" --key "PLAYFAIR EXAMPLE"
ciphers encode columnar "ATTACK AT DAWN" --key ZEBRA
ciphers encode bifid "FLEE AT ONCE" --key BICONDITIONAL --period 5
ciphers encode enigma "ATTACK AT DAWN" --positions MCK --rings BDF --plugboard "AV BS CG"
ciphers encode morse "SOS"                             # ... --- ...
ciphers encode tap-code "HELP"                         # 2 3 1 5 3 1 3 5
ciphers brute "KHOOR"                                  # shift= 3 -> HELLO
ciphers frequency "DWWDFN DW GDZQ" --lang en
ciphers hidden - --pick line < poem.txt                # the first letter of every line
ciphers info enigma

Dashes and stdin

Morse starts with a dash half the time. Every armored PGP message starts with five. Neither spells a flag, so the parser reads it as the text. -.-. stays a C. Once the text is in, a dashed word is an unknown option again. A typo after it fails instead of sneaking into your message. -- and a piped - still work too:

shell
ciphers decode morse '-.-. .- -'                      # CAT
ciphers decode morse -- '-.-. .- -'                   # CAT
echo '-.-. .- -' | ciphers decode morse -             # CAT
ciphers decode openpgp - --key causality < message.asc
ciphers decode morse '...' --kye x                     # Invalid arguments: unknown option "--kye"; takes --shift, ...

- works for the text of every command but crib. On encode and decode it also works for --book. Stdin feeds one argument per call and has to be valid UTF-8. Anything else is refused, not quietly mangled.

Flags

encode and decode read the same flags. A flag the cipher doesn't use is ignored, no complaint. Long names only: there's no -k for --key.

FlagCiphers
--shift <n>caesar
--key <word>vigenere, gronsfeld (digits only), beaufort, porta, autokey, running-key (a passage, a letter per letter of the text), alberti, quagmire-1 to quagmire-4, chaocipher (the left, ciphertext alphabet), playfair, four-square, two-square, hill, polybius, nihilist, fractionated-morse (the alphabet over the triples), columnar, adfgvx, bifid, straddling-checkerboard (the board, 28 cells) and every block and stream cipher (hex, but the passphrase itself for aes-passphrase and openpgp)
--second-key <word>four-square, the keyword for its bottom left square, two-square, for its bottom or right one, quagmire-4, for its cipher alphabet, and chaocipher, for its right, plaintext alphabet
--indicator <word>quagmire-1 to quagmire-4, the keyword that picks the cipher row for each letter
--indicator-under <letter>quagmire-1 to quagmire-4, the plain letter the indicator stands under, A by default
--transposition <word>adfgvx
--square <word>nihilist, the keyword for its square
--rotation <left|right>porta, right for the dCode table
--omit <j|q>four-square and two-square, q for squares without Q as on Wikipedia
--orientation <vertical|horizontal>two-square, horizontal for squares side by side as the ACA and dCode set them
--blanks <digits>straddling-checkerboard
--separator <text>a1z26, - by default
--zero <letter>a1z26, the single digit form
--book <text>book, the whole text to count in, or - to read it from stdin
--address <form>book, word, line-word or page-line-word
--pick <unit>book, word or letter
--start <n>book, 1 or 0
--iv <hex>aes-cbc, aes-cfb, aes-ofb, aes-ctr, triple-des-cbc, rabbit, openpgp (encode only)
--segment <bits>aes-cfb
--block-size <bits>rijndael
--nonce <hex>aes-ccm, aes-ocb, salsa20, xsalsa20, chacha20, xchacha20, chacha20-poly1305
--aad <hex>aes-ccm, aes-ocb, chacha20-poly1305
--counter <n>salsa20, xsalsa20, chacha20, xchacha20, 0 by default
--tag-length <bits>aes-ccm, aes-ocb
--tweak <hex>aes-lrw, aes-xts
--digest <hash>aes-passphrase (md5, sha1, sha256), openpgp (up to sha512, encode only)
--algorithm <name>openpgp, encode only
--count <bytes>openpgp, encode only
--key-length <bits>aes-passphrase
--iterations <n>aes-passphrase
--salt <hex>aes-passphrase, openpgp, encode only
--endian <big|little>rabbit
--bytes <text|hex>block and stream ciphers
--rails <n>rail-fence
--width <n>route
--corner <corner>route, top-left by default
--path <path>route, spiral-clockwise by default
--period <n>alberti, bifid
--letters <n>bacon
--a <n>, --b <n>affine
--positions <ABC>enigma
--rings <ABC>enigma
--plugboard "AB CD"enigma
--preserve-case, --strip-non-alphacaesar, rot13, atbash, substitution, affine, vigenere, gronsfeld, beaufort, porta, autokey, running-key, trithemius, alberti, quagmire-1 to quagmire-4, chaocipher, rail-fence, columnar, route. Case stays by default, --no-preserve-case gives capitals only

brute, frequency, period, guess, crib, hidden and recover take --lang: en, pl or ja, English when you leave it out. period also takes --max-period, 20 by default. Longer key? Raise it. crib takes the ciphertexts as one JSON array, the crib in --crib, the places accepted so far in --known as a JSON array of {"message", "offset", "text"}, and --limit, 10 places by default. hidden takes --pick, --letter, --every, --start and --direction too. It refuses --lang next to --pick. The analysis guide says which goes with which. recover takes the cipher in --cipher, --period for Vigenère and both Beauforts, --key-length up to 9 for columnar, and --limit, 5 keys by default.

Exit codes and output

The output is the text the tool hands a model. For encode and decode that's the result alone, one line, so it composes:

shell
ciphers rot13 "$(ciphers atbash "HELLO")"

--json prints the details instead: the cipher, the direction and every option it ran with. Handy when a default surprised you.

A CipherError prints its message to stderr and exits 1. Same for a missing key, an unknown cipher or a shift out of range, which the schema catches first. No stack trace. Anything else is a bug in the library and still gets one, on purpose. A bug should look like a bug.