Command reference
These commands open the vault and accept --vault and --key to point at a store other than the default: clean, review, restore, map, hook post-tool-use and hook pre-tool-use, mcp, doctor and uninstall. Use this to keep one vault per project. Both flags go after the command that uses them. OBORO_VAULT and OBORO_KEY_FILE do the same job without repeating a flag. These commands never open a vault, and refuse both flags: skill, hook install, models and completions.
clean
Anonymises files into sanitised copies.
oboro clean [PATH]... [OPTIONS]| Option | Effect |
|---|---|
-r, --recursive |
Descend into subdirectories of any directory argument. |
-o, --output <DIR> |
Write outputs into this directory instead of beside each input. |
--stdout |
Write to standard output. Takes a single file. |
--config <FILE> |
Use this configuration instead of the nearest oboro.toml. |
A <PATH> is a file or a directory. Oboro walks a directory for the formats this build reads. It skips hidden entries, symlinks and existing .clean.md, .clean.csv and .clean.tsv outputs, and reports how many files it could not read.
Output is named after the input, with a suffix that follows its format: report.docx becomes report.clean.md, and data.csv becomes data.clean.csv. A workbook becomes one TSV file per sheet. So book.xlsx with a sheet Clients becomes book.Clients.clean.tsv. With --output, Oboro mirrors a directory’s subfolders under it. --stdout works for a workbook only when it holds a single non-empty sheet, since one stream cannot carry several files.
Text can also be piped in, so nothing has to be written to disk first:
printf 'Call 06 12 34 56 78.\n' | oboro cleanPassing - as the path does the same thing. You cannot combine - with file paths. Oboro tidies piped text the same way as a text or markdown file, and cleans it to standard output. It never writes piped text to a file, so --output is refused. Piped text has no name, so filename redaction does not apply.
review
Shows every detection and lets you accept or reject each one before anything is written.
oboro review <PATH>... [OPTIONS]| Option | Effect |
|---|---|
-r, --recursive |
Descend into subdirectories of any directory argument. |
-o, --output <DIR> |
Write outputs into this directory instead of beside each input. |
--config <FILE> |
Use this configuration instead of the nearest oboro.toml. |
A <PATH> is a file or a directory, walked as for clean.
| Key | Action |
|---|---|
j, k, or arrows |
Move through the list. |
g, G |
Jump to the first or last detection. |
space, enter |
Toggle the selected detection. |
a |
Accept every detection. |
n |
Reject every detection. |
w |
Write this document and move on. |
s |
Skip this document, writing nothing. |
q, esc |
Stop, writing nothing further. |
Everything starts accepted. So confirming without touching anything gives exactly what clean would have produced. A document with nothing detected is skipped, rather than opening an empty screen.
restore
Puts real values back into a model’s answer.
oboro restore [FILE] [--stdout]Without --stdout the file is rewritten in place.
An answer can also be piped in, so nothing has to be written to disk first:
pbpaste | oboro restorePassing - as the file does the same thing. Piped text has no file to rewrite, so it always goes to standard output, with or without --stdout.
Oboro leaves a placeholder the vault does not know untouched, and reports it. Such a placeholder is more likely something the model invented than a mapping to recover. Deleting it would corrupt the answer.
map
Inspects or wipes the mapping.
oboro map list [--reveal]
oboro map purge --yeslist shows placeholders and when they were created. --reveal also prints the real values, so it prints secrets to your terminal.
map purge deletes every mapping. Any document you cleaned earlier can no longer be restored, because the values it stood for are gone. The --yes flag is required precisely because there is no way back.
hook
Answers an agent’s hook. It cleans what the model reads before the model reads it.
oboro hook install
oboro hook post-tool-use
oboro hook pre-tool-useYou never run post-tool-use or pre-tool-use by hand. You name them in your settings, and the agent calls them around every matching tool:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Read|Grep|Bash|WebFetch",
"hooks": [{ "type": "command", "command": "oboro hook post-tool-use" }]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "oboro hook pre-tool-use" }]
}
]
}
}The two hooks are inverses, and you need both. post-tool-use controls what the model is shown. pre-tool-use controls what reaches your files. Without pre-tool-use, the model writes [[EMAIL_1]] into your source.
install
Names both hooks in your settings, so you do not have to paste the block above.
--project writes .claude/settings.local.json here. --user writes ~/.claude/settings.json. Without either flag, Oboro asks which you want. With no terminal to ask, it fails and names both flags, rather than choosing for you. --dry-run prints the settings as they would end up, and writes nothing.
The project scope is deliberately the local file, not the shared .claude/settings.json. Turning on a hook that intercepts every Read, Grep, Bash and WebFetch is a decision for whoever sits at the machine. A committed hook that names a binary a colleague has not installed fails closed on every tool call they make. doctor reads both files, so it reports either one.
The plugin names the same two hooks in its own files. Install one or the other, not both. Its hooks go through a wrapper, rather than naming oboro directly, because a plugin can be installed on a machine that has no binary. Without the binary, the wrapper withholds the result and refuses the call itself, with a message saying what to install.
The merge only ever adds. Every other key keeps its place and its order. Other tools’ hooks on the same event are left where they are. Oboro’s group is appended, not substituted.
A hook already naming oboro hook <subcommand> is left exactly as written, matcher included. If you narrowed PostToolUse to Read alone, that is a decision, not drift. Installing again reports it and changes nothing. Oboro decides each event on its own. So a hook you customised does not stop the other half being installed.
Oboro refuses settings that are not JSON, a file whose root is not an object, and a hooks entry of the wrong shape. It names the file in each case. doctor ignores a settings file it cannot parse, since it is only reporting. An installer that did the same would overwrite the file.
Oboro never writes through a symbolic link. It replaces the file by renaming a complete one into place, rather than writing over it where it stands.
post-tool-use
Reads a PostToolUse payload on standard input and writes the reply that replaces the tool’s result.
Oboro cleans the result the same way it cleans any other text, against the vault and the oboro.toml found from the agent’s working directory. A plain string result comes back as a string. A structured result keeps its shape, with every string inside it cleaned.
pre-tool-use
Reads a PreToolUse payload on standard input and writes the reply that replaces the tool’s arguments. So a placeholder the model echoed back becomes the real value before the tool runs.
Oboro restores every string in the arguments, rather than tracking a list of fields per tool. Write carries its text in content. Edit carries it in old_string and new_string. Restoring only rewrites the [[TAG_n]] shape, so a string with no placeholder comes back untouched. Arguments that are not text are left exactly as they were.
A tool call whose arguments hold no placeholder at all is left alone entirely.
Oboro leaves in place a placeholder this vault never issued, and reports it to you, the same as restore does with a document. Such a placeholder is more likely something the model invented than a mapping to recover, so Oboro does not refuse the call over it.
At 17 to 70 ms per call, it is cheap enough to run on every matching tool. See the timings that measured this.
Run oboro doctor to see which halves are installed. Believing they are installed is not the same as having done it.
What you type is never cleaned. The event that fires on a prompt, UserPromptSubmit, can add context to a prompt but cannot rewrite it. So there is nowhere for Oboro to stand between your keyboard and the model. Tool output and tool arguments are covered. Your own words are not. See Limitations.
Object keys are cleaned nowhere either. A tool that keys its result by file path still shows that path. Filename redaction applies to files Oboro writes, not to paths inside a tool’s answer.
If Oboro cannot do its job, the tool does not quietly get its way.
On the way out, Oboro withholds the result. A PostToolUse hook cannot stop the tool, since the tool has already run. A hook that exits non-zero has its reply ignored, which would hand the model the raw text. So a failure exits zero, and answers with a notice in place of the result: a block decision, so the model does not continue on it, plus a message to you naming what broke.
On the way in, Oboro refuses the call, with permissionDecision set to deny. The tool has not run yet, and letting it run would write placeholders into your file.
You are told the detail either way. The model is not. The failure usually names a path, and a path is one of the things a vault redacts.
skill
Writes the skill that tells an agent what the hooks have done to what it reads.
oboro skill install
oboro skill install --with-hooks
oboro skill showThe hooks put [[PHONE_1]] in front of an agent without explaining it. The skill explains what a placeholder is. It says that writing one back verbatim is correct, because pre-tool-use restores it, and that inventing one is not. It also explains what a withheld result or a refused call means.
install writes SKILL.md into a .claude/skills/oboro/ directory. Without --project or --user it asks which:
1 this project /home/you/project/.claude/skills/oboro/SKILL.md
2 every project /home/you/.claude/skills/oboro/SKILL.md
Install the Oboro skill where? [1/2]There is no default, because the wrong scope fails silently: the agent simply never reads the file. With no terminal to ask, the command fails, and names both flags rather than choosing for you.
--with-hooks also names both hooks in the same scope. This is the same merge hook install does, so one command covers what the plugin covers. Oboro plans both halves before it writes either one. So if a scope refuses one half, through a symbolic link or settings it cannot merge into, Oboro installs neither: a skill that describes hooks that are not there is worse than no skill. The question then shows both files a scope would write, and --dry-run shows both too.
The same text reaches an agent two other ways, and both read it from this repository rather than copy it: the plugin, which brings the hooks with it, and npx skills add mcanouil/oboro, which brings only the skill and works for agents other than Claude Code. The second symlinks by default. Oboro never writes through a symbolic link, so install afterwards names the link and refuses.
--dry-run prints the path and stops. show prints the text to standard output and writes nothing.
The text is compiled into the binary. So the skill and the hooks it describes are always the same version.
An installed skill that differs from the one this build carries is left where it is. The new text goes to SKILL.md.oboro-proposed beside it, and Oboro prints the path. So you can compare an edit, rather than lose it. --force overwrites instead.
Oboro never writes through a symbolic link. If .claude, skills, oboro or SKILL.md is a link, the install stops and names it, rather than writing to wherever it points.
mcp
Serves the Model Context Protocol on standard input and output. So an agent in a client with no hook system can ask for a cleaned file, rather than reading the file itself.
oboro mcp --root ~/work
oboro mcp --root ~/work --root ~/contracts
oboro mcp --unconfined| Option | Effect |
|---|---|
--root <DIR> |
A directory clean may read within. Repeat it for more than one. |
--unconfined |
Let clean read any file you can read. |
--config <FILE> |
Use this configuration instead of the nearest oboro.toml. Also read from OBORO_CONFIG. |
You must name at least one --root, or pass --unconfined. A server started with neither refuses to start, and says so. Over this protocol, the caller is a model, not you. A client that offers to remember its approval turns one careless click into a standing licence. So reading the whole disk should never happen just because you left an argument off.
Oboro resolves a root once at startup. So a symbolic link inside a root that points outside it is refused, not followed. A root that does not exist stops the server while you are still watching. A path outside every root is refused in the same words, whether or not it exists. So a model cannot map your disk by asking.
Messages are JSON-RPC 2.0, one to a line. Oboro implements the 2025-11-25 revision. A client that asks for 2026-07-28 is answered with 2025-11-25 instead, not agreed with. That later revision removes the initialize handshake and ping, and requires a server/discover method. This server does none of those. Claiming that revision would leave the client expecting a server this is not.
Two tools are offered.
clean takes a path and answers with that file’s text, cleaned the way clean cleans it, one content block per part. A workbook comes back as one block per sheet, each under a ## <sheet name> heading. So a client that flattens the blocks into a single string still keeps the sheets apart. This tool reads every format this build reads. So a .pdf, .docx, .xlsx, .pptx, .odt or .eml reaches the model as text, which a plain file read cannot manage at all.
map_list takes no arguments and lists the placeholders the vault has issued. It never returns the real values, or the times the placeholders were created. map list does print those times, since a timestamp for each entry is a record of your working hours, not something a model can reason with.
Set --config, or OBORO_CONFIG in the client’s environment. It is worth the trouble. A client launches this server itself, and chooses the working directory it runs in. So the nearest oboro.toml is unlikely to be your project’s. Without one, the server runs on the defaults: no denylist and no custom patterns.
Two lines go to standard error at startup. They name the configuration that was resolved, and which detectors are configured and installed. Most clients keep a server’s standard error in a log. Read it once after wiring the server up: a server that found no configuration says so there, and nowhere else.
It offers the agent a tool. It does not intercept anything. Unlike the hook, which fires on every matching tool call, this server relies on the agent choosing clean over simply reading the file. The agent will not do so reliably. Its value is reach into clients that have no hook system.
clean reads within the roots you named, or every file you can read if you passed --unconfined. Within those roots, cleaning is not confinement. Passwords, API keys and access tokens are not detected kinds, and pass through as written. So keep a root as narrow as the work allows.
Oboro does not offer restoring, because over this protocol the caller is the model. A model that can write a file of placeholders and read it back could use a restore tool to obtain every value the vault holds. That absence is not itself protection: in a client with a shell, the model can run oboro restore --stdout or oboro map list --reveal without involving this server. Restoring stays a command-line action: see restore.
Limitations sets all of this out at length.
models
Fetches and inspects the recognition model. Only present in builds compiled with --features ner.
oboro models pull
oboro models statuspull downloads roughly 348 MB once, and verifies it against pinned SHA-256 hashes. Oboro deletes a file that fails verification, rather than keeping it. Running pull again re-verifies what is already there, instead of trusting it.
doctor
Reports what this build can do and where it keeps things.
oboro doctorvault: /Users/you/.oboro/vault.db
key: /Users/you/.oboro/key
/Users/you/.oboro/vault.db mode 0600 (ok)
/Users/you/.oboro/key mode 0600 (ok)
config: /Users/you/work/oboro.toml
regions: FR, GB (from oboro.toml)
allowlist: 1 entr(y/ies)
denylist: 3 term(s)
patterns: 1 custom
filenames: redacted
formats: txt, text, md, markdown, csv, tsv, docx, eml, odt, pptx, xlsx, xlsm, pdf
ocr: not compiled in; images and scanned PDFs cannot be read
model: installed
network: only `models pull`, and only when you run it
PostToolUse .claude/settings.local.json (Read|Grep|Bash|WebFetch, reachable)
PreToolUse not installed; run `oboro hook install`
skill /Users/you/work/.claude/skills/oboro/SKILL.md (current)
skill /Users/you/.claude/skills/oboro/SKILL.md (not installed)
completion /Users/you/.zfunc/_oboro (current)On Windows, the two protection lines read from the file’s ACL instead of a mode. For example: C:\Users\you\.oboro\vault.db owner-only (ok), or ... inherited access (too permissive) for a file that has not been restricted.
Start here when something is not being detected: it shows whether the model is installed, which configuration was found, and which formats this build reads.
The four lines before the last answer a different question: whether the agent side is wired up.
doctor reports both hook events whether or not they are found. Having only the first is the worst of the three states: the model sees placeholders, nothing puts the values back, and they reach your files. reachable means the program the hook names can actually run. A hook that names a binary not on PATH is configured but useless.
doctor reports an enabled plugin above them, since what it carries lives in its own files, not in any settings file or skills directory. Reporting all four as missing would send you to oboro hook install and oboro skill install for what you already have:
plugin /Users/you/.claude/settings.json (oboro@oboro, enabled)
PostToolUse not in your settings; the plugin carries it
PreToolUse not in your settings; the plugin carries it
skill /Users/you/work/.claude/skills/oboro/SKILL.md (not installed here; the plugin carries its own)
skill /Users/you/.claude/skills/oboro/SKILL.md (not installed here; the plugin carries its own)Avoid installing on top of it: two copies of a hook both run on every matching tool call, cleaning what is already cleaned and restoring what is already restored. oboro hook install and oboro skill install --with-hooks both warn you when they find the plugin enabled, since that is the moment a second copy would appear.
doctor reports both skill scopes for the same reason. current is the text this build carries. edited is a copy that has drifted from it: the state where the agent is told something the binary no longer does.
The completion lines answer the same question for completions. doctor checks every conventional destination for your shell, and names only the ones that hold a file. It regenerates each one and compares it byte for byte against this binary. So current and stale are measured, not guessed. A stale line is followed by the command that rewrites it:
completion /Users/you/.config/fish/completions/oboro.fish (stale)
oboro completions fish --installcompletions
Prints a shell completion script, or installs one.
oboro completions [SHELL] [--install|--uninstall] [--dry-run]SHELL is one of bash, zsh, fish, elvish or powershell. Oboro takes it from $SHELL when you leave it out.
Printing is what happens without a flag. The script goes to standard output, and the destination it belongs in goes to standard error. So one command answers both halves of the question.
oboro completions zsh > ~/.oh-my-zsh/custom/completions/_oboroThe redirect captures the script alone. It leaves a file with nothing in front of it for the shell to trip over, while the instructions still reach the terminal. Add 2>/dev/null to drop them when scripting this.
--install picks the destination and writes it for you. It edits your shell’s configuration only where the file alone is not enough:
oboro completions --install
oboro completions zsh --install --dry-run
oboro completions zsh --uninstallOboro updates a script already on disk where it is, rather than moving it, and sweeps copies left in the other known places. See Shells for every destination, the configuration each shell needs, and how to do it by hand.
PowerShell is the one shell with nothing to write. It evaluates the script from $PROFILE instead, so --install refuses, and says what to run.
oboro completions powershell | Out-String | Invoke-ExpressionA completion script is a copy of the command surface from the moment it was generated. So an upgrade that adds a command leaves it offering the old set. doctor reports when that has happened, and the install script reports it at the moment it happens.
uninstall
Removes everything Oboro wrote: completion scripts, the agent hooks and skill in both scopes, the vault, the recognition model, and the binary itself.
oboro uninstall [--dry-run] [--yes] [--keep-vault]Without a flag, it prints everything that would go and asks you to confirm, the same shape map purge uses. --dry-run prints the same report and stops there. --yes skips the prompt, for a script that already knows what it wants. --keep-vault leaves the vault and its key in place. Without it, they go too, and after that you can no longer restore anything cleaned earlier.
Oboro takes an agent hook out of a settings file, rather than deleting the file itself, since the file is yours. Another tool’s hook on the same event, and every other key, are left exactly as written. A settings file that held nothing else of Oboro’s ends as {}. The skill file and the oboro directory it sits in go outright, since Oboro created and owns exactly that directory. .claude/skills above it does not go, since another skill may still be using it.
On macOS and Linux, the running binary removes itself once everything else is gone. On Windows, the running image is locked. So Oboro prints the path and the command to finish the job instead.
The two settings files Oboro sweeps are the two files hook install writes: .claude/settings.local.json for a project, and ~/.claude/settings.json for a user. A project’s shared, committed .claude/settings.json is different: hook install never writes there, and only a hand-copied snippet puts a hook in it. Oboro leaves that hook exactly as written, since a command that never wrote the file has no business rewriting it for a whole team. Oboro names that hook in the report instead, and doctor marks it too, so a hook that goes on running is not mistaken for one that is gone.
A Claude Code plugin, if one is enabled, and a persisted Docker volume or image, are outside what this command can reach. It names them, rather than pretending they were never there.
oboro uninstall --dry-run
oboro uninstall --yes --keep-vault