0.5.0
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.0The 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 hashesDocker
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.docxThe 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/oboroOn 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.0From 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 TesseractIf 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/oboroThis 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
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
.pptxpresentations, 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 useoboro hook installfor 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/oborothen/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 nooborois onPATH, 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, oroboro 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/oboroas 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 whyoboro skill installafterwards 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;--projectwrites.claude/settings.local.jsonand--userwrites~/.claude/settings.json, without either it asks which, and--dry-runprints 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 hookis 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
hooksentry 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
oborothat is not onPATH, 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 becausepre-tool-userestores it; the text is compiled into the binary so it cannot drift from the hooks it describes,oboro skill showprints it without writing anything, andoboro doctorreports both scopes. - feat: Ask which scope to install the skill into rather than guessing, since the wrong scope fails silently;
--projectand--userskip 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-proposedbeside it so the edit can be compared rather than lost;--forceoverwrites instead, and nothing is written through a symbolic link. - feat: Clean text piped in on standard input, with
printf '...' | oboro cleanor 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,--outputis 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 CodePreToolUsepayload and replaces the tool's arguments, so a placeholder the model echoed back becomes the value again before aWriteor anEdittouches 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 CodePostToolUsepayload on standard input and replaces the tool's result with a cleaned one, so aRead,Grep,BashorWebFetchresult 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 restoreor an explicit-, sorestorecomposes in a pipeline without a temporary file and without the/dev/stdintrick; 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,SIRETandSIRENtook 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.docxis 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
\smatches 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 failedlibphonenumbervalidation 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
.docxentity 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
oboroinvocations sharing a vault no longer fail withdatabase is lockedor aUNIQUE constraint failederror 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 1ends instead of reporting a crash;map listalready did this, and nowcleanandrestoredo too. - fix: Stop quietly in
oboro doctor | head -n 1andoboro models status | head -n 1as 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 1ends instead of reporting a crash, and a command that fails still exits 1 rather than 101; every progress and summary line, inclean,restore,map,reviewandmodels pull, now goes through one writer that drops a line it cannot deliver instead of panicking.