Skip to content

v3.2.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.2.0

Every gate in this project now does what it says it does, and the one documented claim that was a security claim has been corrected.

The release opened by capturing a full measurement baseline against the released v3.1.0 — throughput, wire bytes, cold start, conformance, suites, coverage — before changing anything. That capture is docs/internal/measurements/v3.1.0-baseline.md, and it found four documented claims that were wrong, one of them about the attack surface of the published image.

Eight findings in docs/internal/v3.2.0-findings-log.md.

README and the wiki both said the image contains no shell, and README made it the argument: “the attack surface is the point rather than the size — no shell, no package manager.”

sh /usr/bin/sh apk absent
ash /usr/bin/ash wget absent
busybox /usr/bin/busybox curl absent
npm /usr/bin/npm BusyBox v1.38.0, ID=wolfi

The no package manager half is true. The no shell half is false, npm is present, and the base is cgr.dev/chainguard/node:latest, not the distroless variant the docs called it.

If your threat model assumed no shell, it needs revisiting. Nothing about the image changed in this release — what changed is that the documentation now describes it. Whether to move to a genuinely shell-free base is a real question with real risk, and it belongs in a release that measures it.

Three further numbers were stale in live documents: image on disk (726 MB → 453 MB), the gzipped release tarball (~196 MB → 141 MB), and the production package count (432 → 402 or 384, depending on how you count — the defect there is a document quoting a number without its method).

Gate Was Now
Performance benchmarks “cannot fail on a regression” — its own words Compares median throughput to a committed baseline; fails on a regression and on a baselined task disappearing
Trivy, both workflows CRITICAL only, with a TODO to restore CRITICAL,HIGH — the TODO’s stated precondition was met eight releases ago
Helm chart nothing linted or rendered it helm lint --strict plus four rendered values paths
Operation counts in prose unchecked read from OperationConfig.json by check:versions
Tool metadata from upstream unchecked 7,408 strings screened; blocking in the sync

The benchmark tolerance was measured, and the first measurement was wrong

Section titled “The benchmark tolerance was measured, and the first measurement was wrong”

A gate needs a tolerance and a tolerance has to be measured. The first study reported per-task spreads up to 84% — plausible on a JIT-compiled workload, and wide enough to justify a gate that could never fire. Two causes, both invisible until the numbers were collected rather than printed:

  • A task-name collision. SHA2 is registered twice, for 256 and 512, and both emitted tasks called SHA2 (1KB). The 84% “variance” was two operations taking turns in one slot.
  • JIT warm-up. To Base64 (1KB) measured 7,482 ops/s in a cold process against 11,445–12,188 warm; it is the first benchmark registered, so it absorbs the warm-up.

Fixed, the worst per-task spread across four runs is 9.8% and the median 4.3% — which supported a 25% tolerance, until CI disagreed. Three runs on shared runners produced deltas from −25.5% to +99.1% and two false failures on different tasks, with nothing in the diff touching either. The tolerance is 50%: what a cross-machine baseline can honestly support, firing on factor-level regressions and not on the runner. The missing-task check stays exact.

And the gate’s reach is bounded, which is stated rather than left to be discovered. On its first green CI run the runner measured 27–99% faster than the machine the baseline came from. A regression smaller than that offset therefore passes on CI: the gate catches factor-level regressions there, and tolerance-level ones only on the baseline machine. Still strictly better than a check that said in its own output it could not fail — but “gates” and “gates against what” are different claims. A runner-captured baseline is carried forward.

Two TODO(PR 7) comments — the only two in fork-owned code — narrowed Trivy to CRITICAL with the condition restore once the dependency backlog is cleared to zero, written when that backlog was 272 findings. It reached zero in v2.1.1. Measured before restoring: 0 CRITICAL, 0 HIGH on the published v3.1.0 image.

The PR gate was also quieter than its own report — the Security tab listed HIGHs the whole time while a HIGH could still merge, and the difference between reported and gated is the only part that changes what ships.

Every tool name and description a model reads comes from a tree mirrored verbatim from upstream, and the fork’s review of a sync is a diff review — so the one thing a reviewer cannot catch is the one thing worth screening for. A Unicode TAG-block sequence renders as nothing in a diff.

7,408 strings, four classes screened (TAG-block, control, bidi, zero-width), all zero today. Ordinary non-ASCII — 89 strings with accents and Greek letters — is tolerated deliberately: a rule banning it would fail the first sync adding a cipher name with a diacritic, and then the rule gets removed rather than the metadata fixed.

The planned feature was a response_format: concise | detailed enum across the high-volume tools, citing a published 65% reduction. Measuring this catalogue first said the plan was aimed at the wrong tools:

short transform 55 bytes
describe one operation 3,115 <- the median result
list one category 18,057
search 27,060 <- the outlier
large transform (64 KB in) 87,423 <- the payload IS the answer

A format flag across 504 operation tools buys nothing; their results are the data the caller asked for. The outlier was cyberchef_search, and a generic flag would not have fixed it: it returned the full OperationConfig entry per match — more than cyberchef_describe_operation returns for the same operations. The discovery tool was paying the detail tool’s cost, for operations the caller had not chosen yet.

"base64" 14 matches 27,060 -> 3,087 bytes (-88.6%)

Search now returns names, one-line summaries and an argument count, in the same shape cyberchef_list_operations uses. detailed: true restores the previous payload — opt-in, because the default is what every model pays, and the tool description says what the flag costs.

Terminal window
docker pull ghcr.io/doublegate/cyberchef-mcp_v3:3.2.0

One compatibility change, and calling it anything softer would be wrong. cyberchef_search returns a different default shape:

before [ {name, module, description, infoURL, inputType, outputType, args, ...}, ... ]
after { query, matches, operations: [{operation, summary, args}], next }

A caller that parses the previous default must either adapt or pass detailed: true, which returns the old payload unchanged. Nothing sends that flag automatically, so existing parsers do break until one of those two things happens.

It ships in a MINOR because the opt-in restores the old contract exactly and because an MCP tool result is consumed by a model rather than by a typed client — but the honest description is a response-contract change with an escape hatch, not “additive”. A caller that reads the result as prose, which is the common case, is unaffected and gets a smaller one.

Nothing else in this release changes observable behaviour.

npm run lint zero errors
npm run check:versions versions, package majors and operation counts agree
npm test 241 Node-API + 2,289 operation tests
npm run test:mcp 1,437 tests across 55 files
npm run test:coverage 95.47 lines / 89.89 branches / 96.41 functions / 96.20 statements
npm run conformance both eras match the baseline
npm run benchmark:check no regression against the committed baseline

Every gate added here was proven in both directions before it was committed: the benchmark gate by perturbing the baseline, the count gate by drifting a document, the metadata screens against synthesised positives. A gate that has never been seen to fail is a gate nobody has tested.