Reference

Every command and flag of both tools, and the Canon format in brief. The full format is in spec/CANON.md.

bxm-canon

bxm-canon encode [FILE] [-o OUT]     Canon text (JSON) in, canonical bytes out
bxm-canon decode [FILE] [--pretty]   canonical bytes in, Canon text out
bxm-canon fmt [FILE] [--pretty]      text in, the one normal text form out
bxm-canon check FILE...              exit 0 only if every file is canonical bytes
bxm-canon hash [FILE] [--text]       SHA-256 of the canonical bytes
bxm-canon diff A B [--text]          paths that differ between two values
bxm-canon version

FILE defaults to standard input, and - also means standard input. --text tells hash and diff that the inputs are Canon text rather than canonical bytes. -o writes to a file instead of standard output.

Exit codes: 0 when all is well, 1 when check finds a file that isn’t canonical or diff finds a difference, 2 for any other error.

Diff paths

diff prints one line per difference: + for a path only in the second value, - for a path only in the first, ~ for a value that changed. Paths start at $, with .key for map keys, ["a key"] for keys that aren’t plain identifiers and [3] for array positions.

~ $.version 3 -> 4
- $.tags[1] "b"
+ $["release date"] "2026-10-22"

Canon Format

Tag Type After the tag
00 null nothing
01 false nothing
02 true nothing
03 integer zigzag value as a varint
04 float 8 IEEE 754 bytes, big-endian
05 bytes length, then the bytes
06 string length, then UTF-8
07 array count, then each item
08 map count, then key and value pairs, keys in byte order

Lengths, counts and integers are unsigned LEB128 varints in their shortest form. The decoder rejects:

Error code Meaning
unexpected-end the input ends inside a value
unknown-tag a tag byte outside 00 to 08
overlong-varint a varint longer than it needs to be
varint-too-large a varint that doesn’t fit in 64 bits
nan, infinity, negative-zero floats that aren’t Canon values
invalid-utf8 a string or key that isn’t valid UTF-8
length-too-large a length or count larger than the input left
keys-out-of-order map keys not in byte order
duplicate-key the same map key twice
too-deep arrays and maps nested deeper than 512
trailing-bytes anything after the one top-level value

Canon text

Canon text is JSON with three rules. A number without ., e or E is a 64-bit integer, and one too large is an error rather than a float. A repeated key is an error. Bytes are written h"0aff", with lowercase hex.

C API

int bxm_canon_from_text(const uint8_t *text, size_t len, uint8_t **out, size_t *out_len, char **err);
int bxm_canon_to_text(const uint8_t *data, size_t len, uint8_t **out, size_t *out_len, char **err);
int bxm_canon_check(const uint8_t *data, size_t len, char **err);
int bxm_canon_hash(const uint8_t *data, size_t len, char hex_out[65], char **err);
void bxm_canon_free(uint8_t *p, size_t len);
void bxm_canon_free_str(char *err);
const char *bxm_canon_version(void);

Each function returns 0 on success. Buffers it hands out are released with bxm_canon_free, messages with bxm_canon_free_str. Pass NULL for err to skip messages.

JavaScript

import { load, CanonError } from "bxm-canon";
const canon = await load();
Function What it does
fromText(text) Canon text to canonical bytes (Uint8Array)
toText(bytes, { pretty }) canonical bytes to text; throws if they aren’t canonical
fmt(text, { pretty }) text to its one normal form
check(bytes) throws a CanonError unless the bytes are one canonical value
isCanonical(bytes) the same check as true or false
hash(bytes) SHA-256 hex of canonical bytes
hashText(text) SHA-256 hex of the canonical encoding of a text value
diffText(a, b) the differences between two values, one line each
version version of the Canon core

A CanonError carries code, one of the error codes above or text, and the byte offset. Strings with an unpaired surrogate are refused. To hash a JavaScript object, pass JSON.stringify(obj) to hashText, keeping in mind that JavaScript writes 1.0 as 1, so whole numbers become integers.

Python

import bxm_canon as canon
from bxm_canon import CanonError
Function What it does
encode(obj) Python value to canonical bytes
decode(data) canonical bytes to a Python value; raises CanonError if not canonical
check(data) raises CanonError unless the bytes are one canonical value
is_canonical(data) the same check as True or False
hash(data) SHA-256 hex of canonical bytes
hash_value(obj) SHA-256 hex of a Python value’s canonical encoding
from_text(text) Canon text to canonical bytes
to_text(data, pretty=False) canonical bytes to text
fmt(text, pretty=False) text to its one normal form
hash_text(text) SHA-256 hex of a text value’s canonical encoding
diff_text(a, b) the differences between two text values, one line each

Python values map one to one: None, bool, int (64-bit), float, str, bytes (and bytearray or a byte memoryview), list and tuple (decoded as list), and dict with str keys. A larger int raises OverflowError; NaN, infinity, -0.0 and lone surrogates raise CanonError; other types raise TypeError. CanonError is a ValueError with code and offset, and it pickles.

bxm-memo

bxm-memo run     [flags] -- COMMAND [ARGS...]   restore outputs on a hit, run on a miss
bxm-memo explain [flags] -- COMMAND [ARGS...]   say hit or miss and why; runs nothing
bxm-memo list                                   cached results, newest first
bxm-memo gc [--older-than 30d] [--max-size 2G]  remove old results and unused files
bxm-memo clear                                  remove the whole cache
bxm-memo version

Flags for run and explain

Flag Meaning
-i, --in PATH An input: a file, a directory (every file under it), or a pattern with *, ?, [...] and ** for any number of directories. Repeat for more.
-o, --out PATH An output file or directory the command writes. At least one is needed. Repeat for more.
-e, --env NAME An environment variable whose value is part of the key. Repeat for more.
-n, --name NAME A name for the job, used to explain misses against its last run.
--salt TEXT Extra text in the key, to throw away old results on purpose.
--cache DIR The cache directory.
-q, --quiet No hit or miss lines on standard error.

Everything after -- is the command. Quote patterns so the shell doesn’t expand them first: -i 'src/**/*.go'.

The cache

The cache directory is --cache, or $BXM_MEMO_CACHE, or bxm-memo in the user cache directory (~/.cache on Linux, ~/Library/Caches on macOS, %LocalAppData% on Windows). It holds an entry per result, the last run of each job for explaining misses, and the stored files named by their SHA-256.

gc with no flags removes results older than 30 days. With --max-size it keeps the newest results that fit. Files younger than an hour are never removed, because another run may be storing them.

Exit codes

On run, the command’s own exit code. On a hit, 0. When Memo itself fails, for example on a bad flag or an input pattern that matches nothing, 125. explain exits 0 for a hit and 1 for a miss.

What goes into the key

The command line, the contents and executable bit of every input file and its path, the values of the declared environment variables, the declared output paths, the operating system and CPU type, the salt, and Memo’s cache format version.