Eighteen ciphers. One call.

Caesar to Enigma behind one small API. Encode, decode, brute force a Caesar, count letters. All in your process - no network, no keys, nothing to configure. Library, CLI, MCP server, Pi and OMP extensions. For lessons, and for the puzzle you're stuck on at 1am.

18
ciphers
8
families
5
agent tools
0
network calls

Encode and decode

Same two methods, every cipher

create("vigenere") gives you a class with encode and decode. Each cipher keeps its own option names. Case survives by default, punctuation passes through, and stripNonAlpha flattens the input when a puzzle wants one clean block. This panel walks through 18 ciphers. Nothing here is a recording, the library computes every value in your browser.

  • create(name) returns one cached instance. resolveCipher fixes case and spaces, nothing fuzzier
  • Every result is { text, cipher, operation, options }. One log line tells you what ran
  • Missing key is a MissingOptionError, shift 26 an InvalidOptionError. Never a silent identity
Encode and decode
TScaesar.ts
import { create } from "@agntn/ciphers";

// Caesar Cipher, substitution-shift
const cipher = create("caesar");

const encoded = cipher.encode("ATTACK AT DAWN", { shift: 3 });
encoded.text;   // "DWWDFN DW GDZQ"

const back = cipher.decode(encoded.text, { shift: 3 });
back.text;      // "ATTACK AT DAWN", decode takes the same options
Caesar Cipher · substitution-shift

Brute force

Twenty-five shifts, one line each

A Caesar has 25 keys and the fastest attack is all of them. The list is the current plaintext shifted by 3, then decoded with every shift, the hit in the accent. Same loop behind the CLI command and the agent tool, there was no reason to write it twice.

  • ciphers brute prints every shift. You pick the one that reads as English
  • cipher_brute_caesar returns the same list as text, so a model can pick too
  • Shift 0 is not on the list. An identity is not a decode
Brute force and frequency

$ciphers brute "DWWDFN DW GDZQ"

25 shifts

  1. shift= 1CVVCEM CV FCYP
  2. shift= 2BUUBDL BU EBXO
  3. shift= 3ATTACK AT DAWN
  4. shift= 4ZSSZBJ ZS CZVM
  5. shift= 5YRRYAI YR BYUL
  6. shift= 6XQQXZH XQ AXTK
  7. shift= 7WPPWYG WP ZWSJ
  8. shift= 8VOOVXF VO YVRI
  9. shift= 9UNNUWE UN XUQH
  10. shift=10TMMTVD TM WTPG
  11. shift=11SLLSUC SL VSOF
  12. shift=12RKKRTB RK URNE
  13. shift=13QJJQSA QJ TQMD
  14. shift=14PIIPRZ PI SPLC
  15. shift=15OHHOQY OH ROKB
  16. shift=16NGGNPX NG QNJA
  17. shift=17MFFMOW MF PMIZ
  18. shift=18LEELNV LE OLHY
  19. shift=19KDDKMU KD NKGX
  20. shift=20JCCJLT JC MJFW
  21. shift=21IBBIKS IB LIEV
  22. shift=22HAAHJR HA KHDU
  23. shift=23GZZGIQ GZ JGCT
  24. shift=24FYYFHP FY IFBS
  25. shift=25EXXEGO EX HEAR

Frequency

Count letters before guessing a key

analyzeFrequency(text, "en") tells you whether a ciphertext still has English underneath. A shift keeps the histogram shape and only moves it. A Vigenère flattens it. The panel counts whatever the walk just produced, so watch it collapse when Enigma comes around.

  • Counts sorted by frequency, expected order for English or Polish next to them
  • Index of coincidence near 0.067 is a substitution, near 0.038 polyalphabetic or random
  • No letters in, undefined out. Not an empty histogram
analyzeFrequency

analyzeFrequencycaesar output

12 letters · IC 0.136

  1. D4
  2. W3
  3. F1
  4. N1
  5. G1
  6. Z1
  7. Q1
expected
ETAOINSHRDLC
actual
DWFNGZQ

Ciphers

Eighteen ciphers, eight families

Latin alphabets are A to Z. Playfair and Polybius fold J into I, tap code shares C and K, Bacon uses 26 letters not 24, Enigma is the Wehrmacht M3 with rotors I, II, III and reflector B. Every one of those is a choice, and every choice is written down on the cipher's page. A puzzle answer you can't reproduce is not an answer.

  • Shift, reflection and multiplicative substitutions, three polyalphabetics, one digraph
  • Six fractionations from Polybius to ADFGVX, two transpositions, one Enigma M3
  • Each page lists the options, the keyspace and the conventions, like I and J sharing a cell
All ciphers

Agents

Five tools, three hosts

ciphers mcp serves the tools over stdio, the Pi and OMP extensions render them in the terminal. All three call the same executors, so they answer identically and a fix lands once. Nothing leaves the machine, there is nowhere for it to go.

  • cipher_encode, cipher_decode, cipher_brute_caesar, cipher_frequency, cipher_info
  • Arguments checked against the published JSON Schema before a cipher sees them
  • Text and key lengths are bounded. A model cannot hand the process a novel
MCP, Pi and OMP

toolcipher_encode

MCP · Pi · OMP

input

{
  "cipher": "caesar",
  "text": "ATTACK AT DAWN",
  "shift": 3
}

output

{
  "content": [{ "type": "text", "text": "DWWDFN DW GDZQ" }],
  "details": {
    "cipher": "caesar",
    "operation": "encode",
    "options": { "shift": 3 }
  }
}

Your cipher

Extend Cipher, call register

Every built-in is a concrete class extending the exported abstract Cipher. Yours is the same shape, one file. Throw InvalidOptionError when an option is wrong and let normalizeError wrap the rest. No base class magic, no plugin manifest.

  • name(), info(), encode() and decode(). The same four the built-ins implement
  • register(name, Class) makes it visible to create and resolveCipher
  • Register a name again and the cached instance is dropped, so a hot reload takes
Custom ciphers
TSreverse.ts
import { Cipher, register } from "@agntn/ciphers";
import type { CipherInfo, CipherResult } from "@agntn/ciphers";

class Reverse extends Cipher {
  name() {
    return "reverse";
  }

  info(): CipherInfo {
    return {
      name: "reverse",
      label: "Reverse",
      description: "The text backwards",
      family: "transposition",
      selfInverse: true,
      options: [],
    };
  }

  encode(text: string): CipherResult {
    return {
      text: [...text].reverse().join(""),
      cipher: "reverse",
      operation: "encode",
      options: {},
    };
  }

  decode(text: string): CipherResult {
    return { ...this.encode(text), operation: "decode" };
  }
}

register("reverse", Reverse);

Start with one command

Pre-1.0, so pin exact versions. And these are classical ciphers - fine for a riddle, a lesson or a CTF, useless for a secret. Use a real primitive for those, seriously.

@agntn/ciphers·MIT license· Classical ciphers, for lessons and puzzles. Not for protecting anything, ever.