Skip to content

v0.3.2

Choose a tag to compare

@ghchinoy ghchinoy released this 17 Aug 02:34
bceba9f

0.3.2 (2026-08-17)

Bug Fixes

  • preserve flow/map append entries; fix multi-line flow (#119) (a8ba6f8)
  • require explicit verifier to write verified stamp (#118) (fc3c8c9)

Documentation


v0.3.1 MADE FORWARD PROMISES ABOUT v0.3.2

v0.3.1's release notes did not merely list defects. They promised specific fixes in v0.3.2, and
they closed with a standard we are now bound by:

"binder's central promise is that it never asserts something it has not verified — and that promise
has to bind our release notes too."

A release note that promises a fix is an assertion. Shipping v0.3.2 without reconciling those
promises, item by item, would leave the previous release asserting something false — the exact
failure this release exists to end. Every v0.3.1 promise is reconciled below, including the one we
did not fully keep.


RECONCILIATION OF v0.3.1's KNOWN ISSUES — measured, not assumed

v0.3.1 item Promise made Status in v0.3.2 How verified
1. enrich writes frontmatter binder cannot read back (multi-line flow sequence, orphaned ]) "A fix is landing in v0.3.2." FIXED Re-ran v0.3.1's own published input against the released v0.3.2 binary. No orphaned bracket; binder reads back the file it wrote — a plain re-run reports unchanged. (Repeating the transcript's --verified-by run appends a further attestation under a moving clock, by design — see 3c.) Better than promised: pre-existing entries kept their exact bytes — flow-mapping style, unquoted timestamps, {by,at} order. (#119)
2. Blank line inserted between frontmatter and body "v0.3.2 is landing a change that makes the byte-faithfulness guarantee true" FIXED od -c before/after on a body abutting the closing fence. Body still abuts; first byte unchanged. (#119)
3a. byte-faithful stated unqualified across help text, README, skill docs, references "v0.3.2 addresses all of these." ⚠️ PARTIAL — SEE BELOW #118 corrected the help text, skill and reference docs; #125 corrected the README and user-guide headline and trust claims. Other sites remain.
3b. convert described as deterministic, unqualified "v0.3.2 addresses all of these." FIXED #118
3c. enrich described as idempotent, unqualified "v0.3.2 addresses all of these." FIXED #118
3d. enrich described as adding only absent keys "v0.3.2 addresses all of these." DESCRIPTION FIXED, DEFECT OPEN #118 corrected the wording; the behaviour is #120, deliberately still open and named.
3-atomic. atomic writes stated as holding STILL HOLDS Re-confirmed; the guarantee was not dropped in the edits.
4. BINDER_VERIFIED_BY stamps without an explicit flag "v0.3.2 is landing a change so that binder refuses… and says so." FIXED #118. On the CLI binder refuses and discloses, in prose and in the JSON envelope; the MCP surface never reads the variable at all. Four origin cases re-derived by execution with HOME and XDG_CONFIG_HOME controlled.
5. JSON report classifies a modified trust key as added (no promise made) STILL OPEN #120, carried forward.

The promise we did not fully keep — stated plainly

v0.3.1 said v0.3.2 would address the unqualified byte-faithful claims "everywhere it is stated."
It has not.
The most prominent statements are corrected — the README's opening trust sentence, the
README and user-guide headline guarantees, the user-guide trust-vocabulary claims, the command help
text, the packaged skill and reference docs. Others remain, and we are naming them rather than
letting the earlier promise quietly lapse:

  • README.md — the "How it works" codec description
  • plugins/okf-convert/skills/okf-convert/references/trust-discipline.md
  • internal/enrich/enrich.go — package doc comment
  • and further occurrences; this list is a floor, not a total

Why they were left: the audits that produced the earlier list were keyed on the spelling
byte-faithful, and a second vocabulary — byte-for-byte — was never enumerated at all. Every count
we have published for this has later been found to be a floor. We are not going to publish another
total.
The remaining work is a read-for-meaning pass, not a search-and-replace, and it is tracked
rather than rushed into a patch release.


KNOWN ISSUES — NEW IN THIS LIST

None of the issues below is fixed in v0.3.2. They were found during this release, by probing
the guarantee we were in the middle of correcting.

An earlier draft of this section called them pre-existing and not regressions. That was wrong, and
we are correcting it here rather than quietly.
We went and measured it against the released
binaries instead of reasoning from the code:

#123 is a regression, and we introduced it in v0.3.1. Run the same file through the released
v0.3.0 binary and it comes out intact — the following keys survive, the file still parses, a re-run
reports unchanged. Run it through v0.3.1 or v0.3.2 and the keys are gone. The same is true of the
unreadable-file outcome, and of the block-scalar value corruption. All three arrived in v0.3.1, in
the very commit that was meant to make binder more byte-faithful. They are not long-standing defects
you have been living with; if you were on v0.3.0, this is something we did to you in v0.3.1.

One narrow part of #123 is genuinely older: the case where binder refuses a file as an
unterminated --- block behaves the same way in v0.3.0. Refusing is not the part that hurts you,
and it is not what the rest of this section is about.

#124 we have not dated, and we are not going to guess. Its mechanism is different, we have not
run that measurement, and an unmeasured reassurance is what got this section rewritten in the first
place.

MOST SERIOUS: binder can silently fabricate a trust claim (#132)

This is the most serious defect in this release line. It is listed first deliberately: it needs no
unusual characters, no special flag, and no carriage return. An ordinary Unix-LF file and an ordinary
command are enough.

On a file with Unix (LF) line endings and no unusual characters, adding a new attestation with plain
enrich --verified-by can splice the new entry into the middle of an existing attestation whose
note — or any field — is written as a multi-line block scalar. The existing attestation is severed
from its content, and the orphaned lines are folded into the new attestation's actor identity. The
file then asserts an identity that belongs to no one: for example an actor that parses as
human:alice inner1 inner2, assembled from alice's stamp and bob's note.

binder exits 0, prints no warning, binder validate reports the file conformant, and binder review shows output byte-identical to a healthy file. Neither of binder's own checks reveals it.

It is not only lost bytes — but bytes are lost, and they do not come back. Where more than one
attestation carries a block value, every one but the last loses its text irrecoverably: those
words are gone from the file and gone from the disk. What disappears is the attestation's
rationale — the note recording why something was signed off, which is exactly what people write
in a | block and exactly what has no second copy anywhere.

And on top of that, binder asserts a trust claim no human made — the one failure a provenance tool
exists to prevent.

It is not only the CLI: the MCP convert tool reaches this same defect, and that is measured, not
inferred.
The tool was driven to disk against the released v0.3.2 binary and the result diffed
byte-for-byte against untouched copies of the inputs. Both existing attestations came back with an
empty note.
One of them lost its text outright — those lines are gone from the file, and an
emptied note is indistinguishable from a note nobody wrote.
The other's lines were not discarded:
they were folded into the identity of the stamp the tool had just written. Read back with a YAML
parser that is not binder's, the damaged file does not fail to parse — it yields three
structurally valid attestations, and the third one's actor is the literal string human:probe note2_a note2_b: the stamp's own actor with another attestation's note welded onto it. That is
the same assembly as human:alice inner1 inner2 above, measured on the other surface.
The same
byte-diff comes back empty against the fixed build, so the instrument was reading. On MCP that
stamp can only have come from the verified_by input to the tool call
, because that surface has
no other route to a stamp — it loads no config and reads no environment. So an agent can
destroy the note and fabricate the claim with no human watching an exit code.
There is no MCP
enrich at v0.3.2
; convert is the reached surface, and it is enough. The guidance below is
unchanged and covers both surfaces: stamping is the trigger, whatever does the stamping.

It is a regression: it was introduced in v0.3.1 by the same change that added byte-faithful
preservation, and it is present in v0.3.1 and v0.3.2.

Until a fixed release is available (tracked at #132):

  • If a verified: entry in your corpus carries a | block value, do not stamp that file.
    Stamping is the trigger; reading is not — so you can keep using binder to read your corpus while
    you wait.
  • Do not rely on binder review or binder validate to tell you an attestation is intact — both
    report this corruption as healthy; neither can be your check for it. Checking a file yourself takes
    two different instruments, one for each half of the damage, and they are not interchangeable:
    • For the fabricated identity — reparse the frontmatter with a YAML parser other than binder's and
      read the actor values.
      The damaged file parses cleanly, so "it still parses" tells you
      nothing; the fabrication shows up only as a junk value in an attestation's actor — the stamp's own
      actor with another attestation's note text welded onto it. A check that only confirms the file
      parses will miss this entirely.
    • For the destroyed content — diff against a copy binder never touched. This is required, and it is
      the only instrument that finds it.
      Reparse cannot: a destroyed note reads back empty, and an
      empty note is indistinguishable from a note that was always empty
      — the same reading whether the
      text was destroyed or never existed, so it is not detection. Only a diff against a known-good copy
      tells the two apart.
    • A correct diff is not "no differences." Stamping adds the new attestation you asked for by
      design, so a diff will always show that one addition. Correct means: the only new content is the
      attestation you added; every pre-existing entry is byte-identical; and any changed existing
      entry — or any block-scalar value that shrank or vanished — is the defect.
    • Without a copy made before the run, the destroyed content is undetectable — nothing in the file
      reveals it.
      That is what makes the copy load-bearing rather than nice-to-have. Keep a copy
      binder has never touched — version control counts — and make it before you stamp
      , because after
      the run there is nothing left to compare against.
  • The carriage-return guidance under #123 below does not find this case — it needs no carriage
    return.

A. binder does not recognise frontmatter unless the fence is exact — and then silently rewrites the file (#124)

Revised 2026-08-17 00:26Z, and BOTH revisions widen it. An earlier draft of this section described
this as a BOM and lone-CR issue that invents a title. Independent review measured more on both
axes.
The corrections are stated rather than absorbed, because a known-issues note that quietly
grows is not a known-issues note.

What binder actually requires — grounded in the code, not inferred from probing: the file must
begin with the three bytes ---, immediately followed by LF or CRLF — nothing before it, nothing
between — with a closing --- line after.
The check is a literal prefix comparison: no whitespace
trimming, no byte-order-mark handling.

So the affected set is wider than a BOM. Measured shapes that binder does not recognise:

  • a UTF-8 byte-order mark before the fence;
  • lone-CR line endings throughout the file;
  • the opening fence with a trailing space or a trailing tab;
  • one blank line before the fence;
  • the fence indented by one space.

That list is a floor, not a total, and the space of malformed fences is unbounded. The exact
rule above is the reliable statement; the list is only illustrative.

What happens then: binder treats the file as having no frontmatter, synthesizes a new block,
prepends it, and leaves your original frontmatter in the body as literal text
— including any
verified: attestation it carries.

It synthesizes three keys, not one: title, type, and generated. generated is a
trust-provenance key by binder's own reckoning
— binder refuses --overwrite-keys generated,
saying that overwriting it "would violate the never-fabricate-trust invariant." So binder writes,
into the trust-bearing block, the exact key class it refuses to let you touch — while relocating your
real attestation into the body.

It reports success: exit 0, num_skipped: 0, warnings: []. Nothing is refused and nothing is
disclosed. binder review does not surface it either: the affected file is reported as
unverified with unparsed_frontmatter: [] — the same empty list a clean file reports. The one
report you would check to find this does not show it.

This affects convert as well as enrich — both reach the same codec.

Exactly-conforming LF and CRLF files are not affected by #124, and they are the overwhelming
majority. This statement is about #124 only. A file with an exactly-conforming fence can still be
hit by #123 below, which is more serious — every fixture we built for it had a perfectly
conforming --- fence. The trust-fabrication defect listed first hits conforming files too. Do not read this sentence as
covering anything but #124.

If you keep provenance in frontmatter, check your files against the exact rule above rather than
against the list
— a stray space after the opening fence is enough, and it is invisible in every
editor. Confirm your verified: blocks are still in the frontmatter and not in the body. A fix is
not in this release; it is being handled as its own change so that the detection logic gets a review
of its own rather than riding along with a documentation correction.

B. A stray carriage return in your frontmatter: rewriting a key silently destroys another (#123)

This is a silent, irrecoverable data loss on the trust path.

The trust-fabrication defect listed first is the more serious of the two, on the one axis that is
measured and not in doubt: reach. It needs no unusual characters at all, so it reaches ordinary
files with nothing wrong with them. This one needs a stray carriage return. That is the only
distinction worth drawing between them
— both destroy content irrecoverably, both are silent, both
pass binder's own checks, and neither is the lesser problem.

  • Severity: the keys are destroyed, not moved. #124 relocates your attestation into the
    body, where it can be recovered. This does not.
  • Reach: it is reachable on the default path with no flags, through several routes listed
    below.

You are exposed if your frontmatter contains even ONE stray carriage return — a \r with no
\n after it — whatever your file's line endings otherwise are. A single stray CR is enough in
an ordinary LF file and in an ordinary CRLF file alike. There is no line-ending style that
makes you safe.
Where such a CR is present, the parser and the byte-level splice machinery
disagree about where lines begin, and the codec's span arithmetic is off — even when the opening
fence is exactly conforming.

A single stray CR anywhere in the frontmatter interior can silently destroy another key when binder
rewrites the file
— a key you never named and were not editing. Which key is lost is decided by the
codec's internal span arithmetic, not by you.

Do not try to establish by experiment that your own file is safe. Some arrangements come through
a rewrite intact and some do not, and nothing in the file tells you which you have. A file that
survived one rewrite tells you nothing about the next one: the outcome depends on which key binder is
stamping and on what the frontmatter looks like at that moment, and both of those change.

Changing a key that is not the first leaves a stale duplicate: both the old and the new
value are present. The output parses cleanly — unless the lone CR is inside that key's own block
value, in which case see the unreadable-file outcome below — so the loss is visible only by diffing.

Changing the FIRST key destroys every subsequent lone-CR-delimited key — including a verified:
attestation. They are not moved to the body and not recoverable: binder's atomic write renames the
new file over the original, so exit 0 is the last moment those bytes exist. The run reports exit
0, num_skipped: 0, warnings: [], and names only the key it meant to change.

What triggers it is rewriting the value of the FIRST key — by any route. Adding keys is safe.
These routes reach it today; treat them as what we have found, not as a complete list:

  • --overwrite-keys naming the first key;
  • binder enrich --verified-by <you> — the explicit flag. Measured as the broadest route on
    the CLI: it destroys whether the existing attestation is your own or another identity. It is
    broader than the global-config path because the flag co-signs even a different identity — the
    one case a global-config actor declines — so the case the config path leaves safe, the flag
    destroys.
  • appending a verified stamp when verified is the first key. This needs no flag: if you
    have set verified_by in your global config — the setup we document — plain binder enrich
    destroys the keys. This is the ordinary path for a configured user. It destroys when the
    existing attestation is by your own identity and the new stamp does not collide with an
    existing one. On an ordinary clock it does not collide. Single-user setups are exactly this case.
  • binder convert, for that same configured user, which stamps during conversion and writes a
    bundle binder cannot read back. binder convert can destroy your file in place. convert
    takes an output directory; if the one you give it resolves to the directory your source is in,
    binder rewrites the source itself: the same file, exit 0, no warning. binder convert src -o src does exactly this. A different-looking output path is not necessarily a different
    directory
    — an output path that was a symlink to the source directory destroyed the source in
    testing, with warnings: []. Pointing -o at a file rather than a directory is refused
    (source is not a directory, exit 3) and leaves the source untouched. binder convert --help
    in v0.3.2 says "It never mutates the source." That claim is false, and we have measured it false.

    It is not only the help text. The same promise is made throughout the documentation — in the
    README, the user guide and the tutorial — both in those words and as "never touches the source"
    and "strictly out-of-place." Do not rely on any of them, in any wording. All of these are
    corrected in the next release.
  • The MCP convert tool reaches the same defectso an agent can trigger this without a
    human watching the exit code.
    Its stamping route is its own: the verified_by input to the
    tool call
    , named explicitly on every call and the MCP analog of --verified-by, co-signing a
    different identity exactly as the flag does. No default reaches this surface — MCP loads
    no config for this — so the configured-user path above is CLI-only, and on MCP the actor comes
    from the tool input or from nowhere. The CLI has the identical hole; this is not an MCP-only
    defect, and neither surface is the safe one.
  • There is no in-tool recovery: re-running convert does not bring back a source that has
    already been overwritten, so the only recovery is a copy binder never touched — for example
    one held in version control.

What does NOT reach it, measured: a verified_by set in a repo-local .binder.yaml or in
the BINDER_VERIFIED_BY environment variable never writes a stamp at all, on either surface.
On the CLI binder treats those origins as unauthorized to attest and says so; on MCP they are never
read in the first place, so there is no refusal to disclose. And a global config actor will not
co-sign an attestation belonging to a different identity; it skips the file and names the
existing actor — a CLI rule, because no config default reaches MCP at all.
Neither of these is a safety feature for this defect and neither should be relied on as one
they are trust rules that happen to close two doors. The door they leave open is the common one:
your own prior attestation, co-signed by your own global config.

A second, less severe outcome happens when the lone CR sits inside the multi-line value of the
key being rewritten
— a verified block, rather than a one-line value. Here the keys are not
destroyed. Instead binder writes the file back in a state it can no longer read: the run reports
success, and later runs fail on the file until you repair it by hand. Not every command fails, so
do not expect a uniform wall:
binder lint still exits 0, and binder convert demotes the
frontmatter and reports num_recovered: 1. Recoverable — but note what it means: binder produced
the unreadable file itself.
It is not refusing something you gave it.

This has nothing to do with mixed line endings — that was our earlier reading and it was wrong.
A mixed-ending file with ordinary one-line values destroys rather than bricks, and a file with
no CRLF anywhere still bricks. What decides which of the two you get is where the lone CR sits,
not what other line endings are in the file.

A third outcome is the quietest of all, and it is key loss AND value corruption together. When
the destroyed key had a multi-line block value, the key itself is gone — and its orphaned value
lines are not discarded with it. They are absorbed into the preceding key's value. A
status: stable followed by an orphaned two-item list becomes the single string "stable - x - y".
The file parses cleanly, binder reports the corpus conformant, and the exit code is 0. Nothing
in the output says a key vanished or that a value changed meaning. So you lose the key AND you are
left with a surviving key whose content is now wrong
— a compounding of the first outcome, not a
milder alternative to it, and harder to find by looking than either.

Each further run damages the file again and reports success, appending another attestation to a
file whose other keys are already gone. A re-run whose new by and at exactly match an existing
stamp deduplicates and reports unchanged — which is why a run under a pinned clock can look
harmless. Do not read that as a mitigation: pinning the clock neither prevents the damage nor
reliably hides it.
A pinned clock whose epoch does not match the existing stamp destroys just
the same. The only thing that stops the write is the collision itself.

binder cannot find your exposed files for you, and two of its inspection commands will actively
tell you the file is healthy:
review reports an affected file as human-reviewed with an empty
unparsed_frontmatter list — byte-for-byte the same report it gives an intact file — and
validate reports a bundle whose keys have been destroyed as conformant. Do not use binder validate as your check for this.

Checking whether your frontmatter ended up in the body — the check for #124will not detect this
one
, because here the keys are not in the body either.

What you can do today, before a fixed release exists:

  • Find the files that already carry the trigger. This lists files whose frontmatter contains a
    stray carriage return:

    hits=$(find . -name '*.md' -exec perl -0777 -nE \
      'say $ARGV if /\A---\r?\n(.*?)\r?\n---\r?(?:\n|\z)/s && $1 =~ /\r(?!\n)/' {} +)
    [ -n "$hits" ] && { printf '%s\n' "$hits"
      printf '== %s file(s) at risk\n' "$(printf '%s\n' "$hits" | wc -l)"; } \
      || echo '== 0 files at risk'

    Run it from the top of the tree containing both your corpus and any bundles you have
    generated. This finds at-risk files; it does not certify the rest. It deliberately
    over-reports — it lists every file with a stray CR in its frontmatter, including files that will
    never be rewritten and so would never be damaged. A clean result means nothing was flagged, not
    that your files are safe:
    it does not model #124 at all, and a file it does not list can still
    be affected by that. Treat a quiet result as "nothing found", not as "nothing there."

  • Keep your corpus under version control, or keep a copy binder has never touched. As stated
    above, that copy is the only recovery from this defect — there is no in-tool recovery, and the
    damage is not visible in binder's own reports.

  • Convert frontmatter containing stray carriage returns to LF before running enrich or
    convert against those files. convert copies these line endings into the bundle, so cleaning
    only your source is not enough
    — a bundle you keep stays exposed, and any affected bundle needs
    regenerating.

  • lint, infer, validate, review and graph do not modify your files and are safe to run
    meanwhile. binder index writes index.md.

  • Upgrade to the fixed release once it is available. The fix is tracked at #123. We are
    not naming a version here
    — this addendum exists in part because v0.3.1 named one and we did not
    fully keep it, and we are not going to repeat that in the document that says so.

Same root-cause family as #124, and the same fix cycle. It is a different outcome, though: #124
relocates your attestation, #123 destroys it.

binder's atomic write (temp file + rename) is intact and does what it claims. It is also why #123's
loss cannot be undone: the replacement is renamed over the original, so there is no partial file and
no prior version to recover from.


STILL OPEN FROM EARLIER RELEASES

  • #120 — the binder.report/v1 envelope lists a modified trust key under added. The file
    content is correct; the report's description of it is not. v0.3.2 corrected the documentation of
    this behaviour without claiming the behaviour changed.
  • #63 — configuration shadowing. When BINDER_VERIFIED_BY and a repo-local .binder.yaml are
    both set, binder discloses one refusal naming the environment variable. Whether that is
    sufficient disclosure is open.

WHAT THIS RELEASE ACTUALLY SHIPS

  • enrich no longer corrupts multi-line flow sequences. binder tries to preserve unchanged
    frontmatter entries byte-for-byte, but do not rely on that as a safety property. Several
    distinct defects can alter or destroy pre-existing content during a write — some silently, some on
    ordinary Unix-LF files with no unusual characters at all (see #123 and the cases above). The
    absence of any single trigger, a carriage return included, does not mean a file is safe. The only
    reliable protection is a copy binder never touched.
  • The frontmatter/body separator is preserved — a body that abutted the closing fence still does.
  • BINDER_VERIFIED_BY alone no longer writes a verified stamp. On the CLI binder refuses and
    discloses the refusal
    , in prose and in the JSON envelope; on the MCP surface the variable is
    never read at all. Three routes do write a stamp in v0.3.2, and they do not all work the same
    way.
    Two are explicit — the actor is named on the call itself: --verified-by on the
    CLI
    , and the verified_by input to the MCP convert tool. One is a default: a
    verified_by value in your own global config, which stamps CLI runs that carry no flag of yours.
    The MCP surface has no default route at all — it loads no config for this, so an MCP convert
    stamps only when its caller passes verified_by.
  • The most prominent preservation claims now state their scope — including the limit that #124
    imposes — instead of promising more than the code delivers. Other sites still state it
    unqualified; they are named above and tracked, not fixed here.