Skip to content

0.5.0

Choose a tag to compare

@mcanouil-dev mcanouil-dev released this 30 Jul 15:10
137e208

Install

Quick install (script)

curl -fsSL https://m.canouil.dev/oboro/install.sh | bash

# or pin this exact release
curl -fsSL https://m.canouil.dev/oboro/install.sh | bash -s -- --version 0.5.0

The script picks the archive for your machine, verifies it against SHA256SUMS, and installs into /usr/local/bin when writable, otherwise ~/.local/bin.
It needs bash and curl; on a minimal distribution such as Alpine, install them first with apk add bash curl.

Add --features ner for the build that also finds untold names (on Linux it needs glibc 2.39+), then fetch its model:

curl -fsSL https://m.canouil.dev/oboro/install.sh | bash -s -- --version 0.5.0 --features ner
oboro models pull   # about 348 MB, once, verified against pinned hashes

Docker

docker volume create oboro-vault
docker run --rm \
  -v oboro-vault:/vault \
  -v "$PWD":/work -w /work \
  --user "$(id -u):$(id -g)" \
  ghcr.io/mcanouil/oboro:0.5.0 clean contract.docx

The vault volume is not optional.
Without it the mapping between placeholders and real values disappears with the container, and the document can never be restored.

The ghcr.io/mcanouil/oboro:0.5.0-ner tag carries the ner build with the recognition model already inside the image, so untold names are found with no download and no network at run time.

A prebuilt binary

Pick the archive for your machine from the table below, then:

VERSION=0.5.0
TARGET=x86_64-unknown-linux-musl   # or whichever row matches

curl -fsSLO "https://github.com/mcanouil/oboro/releases/download/${VERSION}/oboro-${VERSION}-${TARGET}.tar.gz"
curl -fsSLO "https://github.com/mcanouil/oboro/releases/download/${VERSION}/SHA256SUMS"

# Check it is what was published.
sha256sum --ignore-missing --check SHA256SUMS

tar -xzf "oboro-${VERSION}-${TARGET}.tar.gz"
install -m 0755 oboro /usr/local/bin/oboro

On macOS, shasum -a 256 --ignore-missing --check SHA256SUMS does the same job.

With Rust already installed

cargo install --git https://github.com/mcanouil/oboro --tag 0.5.0

From source, with the optional features

The default binaries read .txt, .md, .docx, .xlsx and text-based .pdf, and find structured values and anything on your denylist.
They do not find names nobody told them about, and they do not read images.

Names need a -ner archive from the table below (then oboro models pull), the :0.5.0-ner image, or a source build.
Images need the Tesseract system libraries, so reading them stays a source build:

cargo build --release --features ner   # names and organisations, then: oboro models pull
cargo build --release --features ocr   # images and scanned pages, needs Tesseract

If names are not being redacted, a default build is almost certainly why.
oboro doctor reports what any build can do.

To build the optional features without setting up the system libraries on your machine, use the devcontainer, which carries the pinned toolchain, Tesseract and the OCR libraries:

docker build -f .devcontainer/Dockerfile -t oboro-dev .devcontainer
docker run --rm -it -v "$PWD":/work -w /work -u vscode oboro-dev \
  cargo build --release --features "ner,ocr"

In Visual Studio Code or a GitHub Codespace, reopen the folder in the container instead.

Verify what you downloaded

Beyond the checksum, every archive carries build provenance, so you can confirm it came from this repository's workflow and not from somewhere else:

gh attestation verify "oboro-0.5.0-x86_64-unknown-linux-musl.tar.gz" \
  --repo mcanouil/oboro

This tool checks the model it downloads against a pinned hash before using it.
It would be inconsistent to ask you to trust its own binaries on sight.

Which archive is which

Archive For
x86_64-unknown-linux-musl Linux on Intel or AMD. Statically linked, so any distribution, glibc version or Alpine.
aarch64-unknown-linux-musl Linux on ARM, including most cloud instances. Statically linked.
aarch64-apple-darwin macOS on Apple silicon.
x86_64-unknown-linux-gnu-ner Linux on Intel or AMD, with name recognition. Needs glibc 2.39+ (Ubuntu 24.04+, Debian 13+).
aarch64-unknown-linux-gnu-ner Linux on ARM, with name recognition. Needs glibc 2.39+.
aarch64-apple-darwin-ner macOS on Apple silicon, with name recognition.

The -ner archives find untold names once the model is fetched with oboro models pull; the others are the smaller default build.

There is no Windows build.
The code that creates the vault key readable only by you is Unix-specific, and shipping a build where that quietly does nothing would misrepresent what the tool guarantees.

Documentation

https://m.canouil.dev/oboro/


Changes

Features

  • docs: Say what the tests on recognised scans actually assert, now that they also check the text came back rather than only that nothing survived cleaning; two of the six rendered lines is still a long way from measuring how much of a page was recognised, and the page says so. (#85)
  • docs: Say that a PDF page can be read short without anything saying so. A page that cannot be processed is caught and the document refused, but the extractor drops text whose shape it does not recognise rather than failing on it, so a page can come back with less on it than it holds. The per-page floor catches a page reduced to almost nothing; one that loses a line or two is used as it is. (#80)
  • feat: Read .pptx presentations, taking the text of every slide in presentation order, followed by every speaker note and then every comment, so a deck reaches the detectors instead of being refused; slide layouts and masters are left out, since their text is template prompt material rather than anything the author wrote. (#81)
  • feat: Read a scanned PDF rather than refusing it, on a build with --features ocr: the images on its pages go through the recogniser, covering both the way a scanner stores a colour page (DCTDecode) and the way it stores a bilevel one (CCITTFaxDecode), needing no library beyond the Tesseract the feature already asks for; text drawn on the page is kept alongside what is recognised, so a scan with a searchable text layer does not lose half of itself, and a page is refused by name rather than half read when its image is in a codec that cannot be handed over, when a filter is layered over that codec, or when it carries no image at all. (#75)
  • feat: Install both halves with one command, oboro skill install --with-hooks, which writes the skill and names both hooks in the same scope, asks once and shows both files it would write, and plans both before writing either, so a scope that refuses one installs neither; drop the flag for the skill alone, or use oboro hook install for the hooks alone. (#74)
  • feat: Install the hooks and the skill in one step from a Claude Code plugin marketplace, with /plugin marketplace add mcanouil/oboro then /plugin install oboro@oboro; the plugin is this repository, so it ships the skill the binary carries rather than a copy of it, and its hooks go through a wrapper that withholds the result and refuses the call when no oboro is on PATH, rather than leaving a machine unprotected while looking installed. (#74)
  • feat: Report an enabled Oboro plugin in oboro doctor, naming the settings file that enables it, and say of the hooks and the skill that the plugin carries them rather than that they are missing, since what it carries lives in its own files and reporting it as absent would send you to install what you already have. (#74)
  • feat: Say so when oboro hook install, or oboro skill install --with-hooks, is about to name hooks a plugin already carries, since both copies would then run on every matching tool call. (#74)
  • docs: Name npx skills add mcanouil/oboro as the skill-only path, for agents other than Claude Code, saying plainly that it installs the explanation and not the machine and that it symlinks the skill, which is why oboro skill install afterwards refuses the path rather than overwriting it. (#74)
  • feat: Name both hooks in your agent's settings with oboro hook install, so wiring Oboro into Claude Code no longer means pasting JSON by hand; --project writes .claude/settings.local.json and --user writes ~/.claude/settings.json, without either it asks which, and --dry-run prints the settings as they would end up.
  • feat: Merge into the settings file rather than rewriting it: every other key keeps its place and its order, another tool's hooks on the same event are left where they are, and a hook already naming oboro hook is left exactly as you wrote it, matcher included, so a matcher you narrowed by hand survives an install.
  • feat: Refuse rather than replace a settings file Oboro cannot merge into, naming it: invalid JSON, a root that is not an object, or a hooks entry of the wrong shape. Nothing is written through a symbolic link, and the file is replaced by renaming a complete one into place.
  • feat: Say so when the hooks just installed name an oboro that is not on PATH, since a hook the agent cannot run fails closed on every matching tool call.
  • feat: Tell an agent what the hooks have done to what it reads, with oboro skill install, which writes a skill explaining that [[EMAIL_1]] is a real value it cannot see rather than a bug or a template, and that writing the placeholder back verbatim is correct because pre-tool-use restores it; the text is compiled into the binary so it cannot drift from the hooks it describes, oboro skill show prints it without writing anything, and oboro doctor reports both scopes.
  • feat: Ask which scope to install the skill into rather than guessing, since the wrong scope fails silently; --project and --user skip the question, and with no terminal to ask the command fails and names both flags.
  • feat: Leave an edited skill where it is, writing what would have been installed to SKILL.md.oboro-proposed beside it so the edit can be compared rather than lost; --force overwrites instead, and nothing is written through a symbolic link.
  • feat: Clean text piped in on standard input, with printf '...' | oboro clean or an explicit -, so a caller holding text in memory no longer has to write it to a temporary file first; the cleaned text goes to standard output, --output is refused since there is no name to write alongside, - cannot be combined with file paths, and input that is not valid UTF-8 is refused rather than mangled.
  • feat: Report whether the agent hooks are installed in oboro doctor, naming the settings file, the tools each is matched against, and whether the program it names can be run; both events are reported even when neither is found, since having only the cleaning half means placeholders reach your files.
  • feat: Put real values back into what an agent writes with oboro hook pre-tool-use, which answers a Claude Code PreToolUse payload and replaces the tool's arguments, so a placeholder the model echoed back becomes the value again before a Write or an Edit touches a file; every string in the arguments is restored whatever field it sits in, arguments holding no placeholder are left alone, a placeholder the vault never issued is reported and left in place, and a failure refuses the call rather than writing placeholders into your source.
  • feat: Clean what an agent reads with oboro hook post-tool-use, which answers a Claude Code PostToolUse payload on standard input and replaces the tool's result with a cleaned one, so a Read, Grep, Bash or WebFetch result reaches the model as placeholders; a structured result keeps its shape with every string in it cleaned, and when cleaning fails the result is withheld rather than shown, with the reason reported to you and not to the model.
  • feat: Restore an answer piped in on standard input, with pbpaste | oboro restore or an explicit -, so restore composes in a pipeline without a temporary file and without the /dev/stdin trick; piped text has no file to rewrite, so the restored text always goes to standard output.

Bug Fixes

  • fix: Read an identifier grouped with a no-break space. IBAN, CARD, SIRET and SIREN took an ordinary space between groups and nothing else that reads as one, so an IBAN or a card number written with U+00A0 or U+202F was not detected at all and reached the output whole. French typography groups numbers this way and a word processor inserts them on its own, so a .docx is exactly the input that carries them. The separator is now decided once and shared by every pattern, phone number included, and still excludes line terminators, since a candidate crossing a line merges two values into one that validates as neither. (#83)
  • fix: Look for the value inside a candidate a checksum rejects, rather than discarding the candidate whole. A pattern that spans a range of lengths absorbs whatever follows it, so a phone number with an address after it on the same line, or a card number with stray digits after it, formed one candidate that validated as neither and reached the output untouched. The shorter spans are tried leftmost and longest first, the same order the pattern itself is matched in, and only the span that validated is redacted, so the house number stays in the text and the address is still found. Two numbers written side by side are now both found, where the first used to consume the start of the second. (#82)
  • fix: Narrow the separator between the digits of a candidate phone number so it can no longer cross a line break, since \s matches a newline: a valid number at the end of a line was merging with a leading digit on the next, such as an address or a date, into a longer candidate that then failed libphonenumber validation and was discarded whole, so the number as actually written reached the output undetected. (#81)
  • fix: Decide page by page whether a PDF needs reading as a scan, rather than judging the document as a whole: a scanned page in an otherwise textual PDF used to be carried along by the text on the other pages and was never recognised, so whatever it held never reached the detectors. A page carrying a few words and no image, such as a section divider, is kept as it is rather than refused, and a document whose extraction stops part way through is now refused instead of coming back as though it were the whole. (#77)
  • fix: Bound the names a document supplies before they reach an error message, so a file of someone else's making no longer decides how long an error is or which control characters reach your terminal; this covers a .docx entity and archive part, a spreadsheet's sheet name and a PDF image filter, and a sheet name is shortened for the further reason that it is one of the places personal data turns up. (#76)
  • fix: Allocate placeholders under one atomic step, so oboro invocations sharing a vault no longer fail with database is locked or a UNIQUE constraint failed error when two of them meet the same new value at the same moment; each value still maps to exactly one placeholder.
  • fix: Stop quietly when a reader closes the output pipe, so oboro clean note.txt --stdout | head -n 1 ends instead of reporting a crash; map list already did this, and now clean and restore do too.
  • fix: Stop quietly in oboro doctor | head -n 1 and oboro models status | head -n 1 as well, the last two commands whose output was still written with macros that crash on a closed pipe.
  • fix: Stop quietly when a reader closes the error pipe too, so oboro clean notes/ 2>&1 | head -n 1 ends instead of reporting a crash, and a command that fails still exits 1 rather than 101; every progress and summary line, in clean, restore, map, review and models pull, now goes through one writer that drops a line it cannot deliver instead of panicking.