v3.3.0 release notes
Release Date: 2026-09-04
Upstream Base: CyberChef v11.4.0 (unchanged)
Licence: GPL-3.0-or-later
Node: >=24 <27
Image: ghcr.io/doublegate/cyberchef-mcp_v3:3.3.0
Highlights
Section titled “Highlights”Twelve new analysis tools, 4 registry tools to 16 — and none of them is a tool the plan asked for. The charter’s own kill criterion fired on the phase it proposed next, so the scope was re-derived by reading the reference projects as code rather than as documentation. What that turned up is the substance of this release, and most of it contradicts what those projects document about themselves.
Also: the two things v3.2.0 shipped knowing they were incomplete.
Eleven findings in docs/internal/v3.3.0-findings-log.md.
The tool programme’s kill criterion fired
Section titled “The tool programme’s kill criterion fired”The v3.3.0 charter is the ext-proj-int programme, and its first kill criterion says to re-check
OperationConfig before building, “the catalogue grew 440 → 504 during v2.x”. Checked:
| Sprint 2.1 tool | Upstream today |
|---|---|
cyberchef_encoding_morse |
From / To Morse Code |
cyberchef_encoding_braille |
From / To Braille |
cyberchef_encoding_ascii85 |
From / To Base85 — ASCII85 is Base85 |
cyberchef_cipher_enigma |
Enigma |
cyberchef_cipher_affine |
Affine Cipher Encode / Decode |
cyberchef_cipher_bacon |
Bacon Cipher Encode / Decode |
cyberchef_encoding_baudot |
not present |
cyberchef_cipher_playfair |
not present |
cyberchef_cipher_polybius |
not present |
Six of nine already exist. Building them adds no capability and six names would shadow working
operations — registration throws on that, so three would not start the server. Sprint 2.2 fails a
different constraint the charter states two sections later: it names cyberchef_recipe_list, which
is a shipped v1.6.0 tool.
The programme is not dead, and re-deriving it did not end the release — it ended the charter’s version of it. The three tools genuinely absent from the catalogue shipped, along with nine the charter never proposed.
What reading the reference projects as code turned up
Section titled “What reading the reference projects as code turned up”The charter treats Ciphey and Ares as designs to port. Read as code, all three of their advertised search intelligences are absent, unused, or mis-scaled:
| Documented | Actual |
|---|---|
| Ciphey uses A* search | basemods/Searchers/__init__.py is one line and never imports astar. Dead, and non-functional anyway — self.text is read and never assigned. |
| Ares ranks by decoder popularity | It runs A*, but both production call sites pass &None for the decoder argument (astar.rs:306, astar.rs:386); only unit tests pass Some. Popularity, success-rate adaptation and the pairing penalty never fire. |
| Brandon does a chi-squared test | Its 40-line docstring describes one. check() is a dictionary-proportion test and nothing else. |
Ares also carries railfence popularity 5.0 on a 0–1 scale, which computes to -4.0 and would
dominate the queue — a latent bug masked only by the &None defect above.
What carries both tools in practice is the boring pruning: minimum length, printable ratio, exact
dedup. That is what plaintext_check does, in the order the cheap checks resolve, without
pretending to a ranking model that would not run.
CyberChef’s own Magic has the opposite profile. It has the hard depth cap (3) that neither of them
has, and lacks both things that do the work: no visited set at all, and no structural quality
gate. Its _chiSqr is fed percentages rather than counts, so it is scale-free and ignores sample
size, and its prune test probability > 0 is a floating-point underflow boundary rather than a
threshold. Recorded, not fixed: Magic lives in src/core, which is mirrored from upstream.
The twelve tools
Section titled “The twelve tools”Two solvers that cannot be operations. Vigenère Decode takes a key and Substitute takes a
mapping; nothing finds either, because the search is a loop with a decision inside it and a linear
recipe has no way to express one.
vigenere_breaktreats the index of coincidence as a filter, not a judge. It cannot separate a length from its multiples at all — Practical Cryptography’s ownCIPHERSexample scores period 14 (0.0988) above period 7 (0.0910) — so a shortlist of lengths is solved in full and the plaintext’s trigram score decides. Committing to one length first cost two of ten cases; the shortlist recovered both. 9 of 10, and on the tenth it reports its own failure rather than a key.substitution_breakis Practical Cryptography’s method with its published parameters, and it is hill-climbing with restarts, not simulated annealing — that site publishes no temperature schedule anywhere. Measured on held-out prose: 83.6% of letters at 150 characters, 91.2% at 250, 95.9% at 350. Stated as recovering most of the mapping and leaving a pair or two swapped, because only 9 of 12 reach 95% of letters at 350 and calling 95.9% a solve would be the dishonest reading.known_mappingpins letters out of the swap pool, so pinning what you can read and re-running is a real constraint rather than a hint the search discards.
classical_cipher — Playfair, Polybius, ADFGVX and Baudot/ITA2, all verified absent from
src/core/operations/. Every published vector reproduces exactly, including dcode’s DCODE BAUDOT!
bit string in both directions. They are registry tools rather than operations for a fork-hygiene
reason: src/core/** is mirrored from upstream and registering an operation also touches
Categories.json, so each would be a patches/fork patch that has to keep applying forever.
Three conventions are parameters rather than choices, because implementations that disagree on one disagree on every message: Playfair’s 26th letter (merge I/J is canonical, dcode omits V), Polybius coordinate order, and the figures shift — ITA2 and the US teleprinter table differ in exactly six cells, and dcode ships the latter under the former’s name.
rsa_multi_key — batch GCD, common modulus, Håstad broadcast and Franklin–Reiter: attacks that
need a SET of keys. Three of the four recover the message without factoring anything, and the output
says so rather than letting “plaintext” read as “the key is broken”. Two details worth stating:
batch GCD’s degeneracy detector is quotient === 0n rather than gcd === modulus (a comparison
against a small value, and the cause rather than the symptom), and Franklin–Reiter’s polynomial gcd
in Z_N[x] surfaces a non-invertible leading coefficient as a factorisation, which is a better
result than the message it was computing.
corpus_diff — the other thing Fork structurally cannot do: a statistic across branches.
Per-offset byte and bit variance (a packed flag moves one bit and leaves the byte merely
“variable”), ECB detection reporting where the repeats sit, and nonce reuse emitting the XOR of
the two bodies — which is both the evidence and the way in, and is exactly what crib_drag takes.
crib_drag — C1 xor C2 = P1 xor P2, one algebra serving two attacks. Its strongest filter
appears in no published write-up of the technique: in the single-ciphertext repeating-key case a
true hit at offset i must agree with the key at i mod k, so a fragment longer than k has to be
internally periodic. A wrong offset is often printable; it is hardly ever periodic.
plaintext_check, entropy_scan, jwt_weakness, hash_crack,
hash_statistics and timestamp_identify — see the CHANGELOG for the one-line versions.
Three of them turn on a caveat rather than a feature: entropy’s 7.0 threshold is folklore (the
sourced Lyda–Hamrock rule is a conjunction of 6.677 and 7.199, and NDSS 2020 calibrated 7.0 as
roughly what XOR with a three-byte key produces); half the well-known JWT attacks depend on what a
server does and are reported as headers present rather than as findings; and hash_crack refuses
bcrypt, scrypt, Argon2 and yescrypt by name, because a pure-JS attempt would run, find nothing,
and imply the password was strong.
Two things that were nearly wrong, and one that was
Section titled “Two things that were nearly wrong, and one that was”A statistic you can compute is not a statistic you can decide on. plaintext_check called a
52-letter ROT13 sentence “not plaintext” on an index of coincidence of 0.0332. That is backwards: a
monoalphabetic substitution preserves the IoC, so ROT13 of English reads English’s. The sourced
50-letter floor is where the statistic becomes computable; deciding on it needs about 200, and the
same text at 312 letters measures 0.0487.
Quadgrams from a small corpus are worse than trigrams. Built both ways from the same 1,328,077 letters and measured on 300-letter ciphertexts: trigrams 67.9%, quadgrams 56.1%. At 11.75% coverage of quadgram space the floor dominates. A threshold calibrated against one model is meaningless against the other — and so is a model of one order built from a corpus sized for another.
The trigram generator was eating its own output. Base64 contains //, the comment scanner
matches //.* to end of line, and the table’s encoded bytes were being read back in as English
prose. Two consecutive runs produced corpora differing by fifty letters, which is how it was found.
The default surface doubled, and that is the cost of the release
Section titled “The default surface doubled, and that is the cost of the release”| surface | v3.2.0 | v3.3.0 |
|---|---|---|
index (default) |
28 tools, 20,297 bytes | 40 tools, 40,637 bytes |
curated |
106, 83,543 | 118, 103,883 |
all |
531, 400,701 | 543, 421,041 |
Registry tools have no navigation path: cyberchef_bake runs recipes of operations, and the
three navigation tools walk OperationConfig. A registry tool that is not listed cannot be called
at all, and listing must never be stricter than dispatch.
Trimming prose out of tool and field descriptions recovered 3.8 KB of 9. The alternative was
considered and rejected: one indexed dispatcher taking a tool name and a free-form args object
would have recovered 5 KB more and reintroduced exactly the defect v2.1.0 was spent fixing — a tool
whose schema tells a client nothing. index + one operation schema remains 9.9x cheaper than
all, which is the property the surface exists to provide.
Errors stop pointing away from the fix
Section titled “Errors stop pointing away from the fix”Carried from v2.9.0, deferred twice. Measured through a real client:
Error [INVALID_INPUT]: Unknown argument for "AES Encrypt": input_type
Context: - hint: "Valid arguments: key, iv, mode, input_arg, output, ...include_iv_in_outp..."
Suggestions: 1. Verify input data format and encoding 2. Check for special characters or invalid byte sequences 3. Ensure input matches the operation's expected formatTwo defects, and the second is worse than “generic”:
- The advice contradicts the error. The failure is a wrong argument name; the advice is about
data format and special characters.
ErrorSuggestionsis keyed by error CODE, so all 504 operations share three lines for everyINVALID_INPUT. Advice pointing away from the fix is worse than none — it is the model’s next action, and it sends the caller to inspect an input that was fine. - The truncation cut the fix. A flat 100-character cap on context values chopped
hintmid-word, at exactly the field that says what to do.
Generic suggestions are now suppressed when a specific hint exists, not removed — a Gunzip
failure on non-gzip input has nothing better to offer and keeps all three lines. Guidance keys get
600 characters and say when they were cut.
663 → 540 bytes, and the useful half is no longer truncated.
The benchmark baseline can be captured where the gate runs
Section titled “The benchmark baseline can be captured where the gate runs”v3.2.0’s regression gate ships at a 50% tolerance and said plainly that this is a stopgap: the baseline came from a developer machine, the gate runs on a GitHub runner, and against the same baseline that runner produced deltas from −25.5% to +99.1% and two false failures at 25%.
benchmark-baseline.yml captures the baseline on the runner. Three decisions in it are what
separate a baseline from a moving target:
- Manual and scheduled, never on push. One that regenerates every commit can never report a regression.
- It opens a pull request. Moving a gate’s reference point silently is indistinguishable from switching the gate off.
- Same method as the local capture — five runs, first discarded — so the cold-run artefact that made the first variance study wrong by an order of magnitude cannot reappear.
The tolerance is deliberately not lowered here. It should move on a runner-captured measurement, not on the expectation of one.
arm64 works; its performance is still unmeasured
Section titled “arm64 works; its performance is still unmeasured”Driving the published v3.2.0 image on both platforms through a real MCP client:
linux/amd64 boot + tools/list 775 ms MD5 (10KB) 1.7 mslinux/arm64 boot + tools/list 10412 ms MD5 (10KB) 32.4 msThose arm64 numbers measure QEMU, not arm64. The host is amd64, so the 13x boot penalty is the emulator’s. Publishing them as arm64 performance would be a measured-looking number about the wrong thing.
What they establish is what nobody had checked: v2.8.0 shipped the arm64 image and CI asserts the
optional @napi-rs/nice arm64 binary resolved, but no test had ever connected a client to it.
It boots, lists tools and bakes correctly. The gap narrows from “unverified and unmeasured” to
“verified, unmeasured”; the rest needs arm64 hardware.
Upgrading
Section titled “Upgrading”docker pull ghcr.io/doublegate/cyberchef-mcp_v3:3.3.0No breaking changes. Two things a caller may notice:
- The default
tools/listpayload doubled, 20,297 → 40,637 bytes, because twelve new registry tools have to be listed to be callable.CYBERCHEF_TOOL_SURFACEis unchanged andindexis still the smallest of the three. - Error text is different where an error carries specific guidance — shorter, and without three
generic lines that contradicted it. Anything matching on the literal
Suggestions:block for an unknown-argument error will no longer find it; the actionable content is inContext.hint, complete rather than truncated.
xor_key_length’s key_guess changed shape: space_share is replaced by a per-column columns
array carrying each key byte’s chi-squared score, its runners-up and its margin. The previous
recovery method is available unchanged behind assumed_common_byte: 32.
Verification
Section titled “Verification”npm run lint zero errorsnpm run check:versions versions, package majors and operation counts agreenpm test 241 Node-API + 2,289 operation testsnpm run test:mcp 1,611 tests across 66 filesnpm run test:coverage 95.56 lines / 89.52 branches / 96.51 functions / 96.58 statementsnpm run conformance both eras match the baselinenpm run benchmark:check no regressionAnd against the artefact, not the branch. The image was built and driven through a real MCP
client: 40 tools listed, and classical_cipher, substitution_break, hash_crack,
rsa_multi_key, entropy_scan and jwt_weakness all answering correctly under production-only
dependencies. 475 MB, byte-for-byte the same as the published v3.2.0 image — the trigram table
is 46 KB and nothing else was added.
Every one of the sixteen registry tools is now exercised through the SDK client rather than by
calling run() directly, with a companion test asserting the fixture list matches the registry.
That is the v2.1.0 lesson applied at the right scale: a direct call touches none of the schema
conversion, the capability hand-off, the timeout wrapper or the content block.