Redaction
Redaction replaces sensitive content (people, unreleased product names, internal roles, secrets) with protected placeholders before a document is sent for AI translation or handed to an external translator, then restores the originals once the translation comes back. The sensitive value never leaves the local machine.
For the architecture and design decisions behind this, see C-10: Content Redaction.
In the CLI, redact and unredact are pipeline steps. They run in four places:
- the built-in
secure-translateflow runs redact, translate, unredact (kapi run secure-translate); - a project flow can place the same steps around its own pipeline;
kapi extract --redactprotects a translator hand-off;- in a project that declares a redaction policy,
kapi exec <tool>puts the two steps around a tool that calls a model, for each file under the policy that covers it (see Recipe configuration).
Under kapi exec, the steps apply the policy's rules to every block the tool
reads, blocks marked not translatable included, to each translation beside the
source, plural forms included, and to everything the tool sends with them (see
What the policy covers). A policy that detects
entities stops the run, because no step ahead of the tool finds them. A tool
with no declared contract does not run under the policy, because kapi cannot
tell whether it calls a model. See
Understanding the CLI layers for the flow and
single-tool surfaces.
The policy also covers external-command, because the program it runs may
send what it reads anywhere. The program reads each withheld value as its
placeholder, and kapi puts the original back into the program's output. This
holds for kapi exec external-command and for a flow, which needs a redact
step ahead of the tool. When the output drops, alters, repeats or adds a
placeholder, the block keeps its content and the run reports a failing finding
that names the block and the placeholder, and kapi exec and kapi up exit 3.
What the policy covers
Under a declared policy, the policy's rules apply to every piece of the project a request to a model carries:
- the blocks the tool reads, blocks marked not translatable included, each translation beside the source, and every plural form;
- the blocks before and after a block, sent to show where it sits;
- the block's key in its document;
- the block's previous version and its approved translation, read from the content memory;
- the term rules: each term, its replacement, and the do-not-translate terms;
- the voice profile: its guidance, style, patterns and examples;
- the instruction, from
--instructionor the recipe; - the domain named for term extraction and voice inference;
- the text an MCP tool call passes, and the translation passed beside it.
This holds for kapi exec, for each step of a flow that runs after its
redact step (kapi run, kapi up), for the MCP tools, for Deeper
look with AI, and for the AI actions in Kapi Desktop's review (Fix with AI,
Retranslate, Explain and the AI pre-review). A value a block withholds
reads as the same placeholder wherever the request carries it. A value that
appears only beside the block, in a term rule say, gets a placeholder of its
own, and kapi restores it when the model copies it into a translation. A tool
that reads text alone, such as term extraction, review or voice inference,
reads each withheld value as its visible placeholder ([REDACTED:Person]).
Entity detection reads the blocks and does not reach the rest of a request. A person named only in the voice profile or a previous version is redacted there when a rule names them, or when a block of the same request withholds the same text.
kapi voice check --ai applies the policy of the file it reads, or
defaults.redaction for text given inline. kapi terms expand applies the
rules of defaults.redaction to the terms it sends, and writes no forms for
a term the policy withholds.
The rules cover text only, so they do not reach the image crop or speech clip
media-refine sends. A machine-translation engine from a plugin reads
the redacted block, each withheld value as a placeholder element it keeps in
place, and nothing else from the project.
How it stays local
A redacted span becomes a placeholder, the same protected inline-code
primitive kapi uses for variables and tags, with a type like
redaction:person. The placeholder carries only a category and a stable token;
the original text is held in a local vault and is never written into the
content that leaves the machine.
For AI translation the protection is structural: kapi sends the model an opaque
token (<x id="…"/>) in place of each placeholder, so the model receives
neither the original text nor its visible label, then re-attaches the
placeholder by token when the translation returns. For external translation the
placeholder travels in the bilingual file as ordinary protected markup while
the original↔token mapping stays behind in a gitignored sidecar.
Detection
Redaction finds sensitive spans with one or both detectors:
-
Rules (default) are literal terms and regular expressions you declare. Fully offline and deterministic.
-
Entities (opt-in) are named entities (people, organizations, products, locations, dates, …) recognized automatically and redacted by category. A fast local model keeps detection on the machine; a cloud model trades that for broader coverage during the detection step. The same detection feeds entity-generalized content-memory reuse, so one annotation serves both.
The
entity-extractstep selects the detector withengine:llm(the configured AI provider; the default),ner(an on-device model, with no provider call, no credentials and nothing leaving the machine), orhybrid(both, merged). Withengine: nerthe step carries no remote-egress side effect, so the placement pass accepts it ahead ofredactwithout qualification. The browser Flow workspace lab runs this end to end with a GLiNER ONNX model loaded in the page.
Each match is assigned a category. The recommended categories are person,
role, product, org, location, and custom, but categories are
free-form, so use whatever the placeholder template should display.
Rules file
The term list is itself sensitive, so it lives in a dedicated file outside the committed recipe. Keep it gitignored:
# redaction.yaml: bind it with defaults.redaction.rules, and gitignore it
version: v1
placeholder: "[REDACTED:{category}]" # {category} and {n} are substituted
detectors: [rules]
rules:
- term: "Sarah Chen"
category: person
- term: "Project Halcyon" # unreleased codename
category: product
- pattern: "ACME-[A-Z]+-\\d+" # regular expression: internal SKUs
category: product
flags: [ignorecase] # also: wholeword (literal terms)
Workflows
In-process: secure-translate
The built-in secure-translate flow redacts, AI-translates against the
placeholders, and restores the originals, all in a single run, so the secret
is only ever in memory:
kapi run secure-translate -i src/locales/en.json --target-lang fr
redact is an ordinary transformer step: the framework
applier rewrites the source, replacing sensitive spans with placeholders and
vaulting the originals fail-closed, before the next step observes it. Because
redaction's edit plan is structured, run-anchored overlays attached upstream
(terms, entities) survive the rewrite by rebasing; only spans overlapping a
redacted span are dropped. The flow's placement pass keeps the ordering safe:
it rejects a redact placed after any step that sends source to a remote
service. unredact is an ordinary late step that restores the originals on
the way out. You can compose both into your own flows as ordered steps:
steps:
- tool: redact
- tool: translate
- tool: unredact
In a project, a redact step that names no rulesPath or rules of its own
redacts each file under the policy the recipe declares for that file. A step
that names its own rules keeps them.
External: extract, translate, merge
When a human translator or CAT tool does the translation, redact on the way out
and restore on the way back. kapi extract --redact emits a bilingual file
containing only placeholders and writes the originals to a local vault sidecar;
kapi merge restores them after the translator returns the file.
# Emit redacted XLIFF: originals stay in .kapi/vault/batches/<batch>.json
kapi extract --redact
# ... translator fills in targets, preserving the placeholders ...
# Restore the originals into the merged target
kapi merge -i out/app.en-to-fr.xliff
Pass --redact-rules <path> to point at a rules file directly, or enable
redaction in the recipe (below) so plain kapi extract redacts. Use
kapi merge --no-restore to keep the placeholders in the merged output.
Recipe configuration
Declare redaction in the project recipe, and kapi applies it wherever content
leaves the project: kapi push, kapi exec, kapi extract, Deeper look
with AI and the record kept of each edit. A policy can be declared in three
places:
defaults.redaction, for every file of the project;redaction:on a collection, named or bare, for the files it holds;redaction:on a content item, for the files that item matches.
Each file is redacted under the most specific declaration that covers it: its
item's, else its collection's, else the defaults. That declaration replaces the
others for the file. Its fields are not merged with theirs, so a collection
that names its own rules file redacts with that file alone, and one that
needs the default rules as well lists them in its own rules file. A
redaction: at any of the three places that sets rules, detectors or
placeholder must set enabled: true, and the recipe fails to load if it
does not; enabled: false alone turns redaction off for those files.
defaults:
source_language: en
target_languages: [fr, de]
redaction:
enabled: true
rules: redaction.yaml
detectors: [rules]
placeholder: "[REDACTED:{category}]"
collections:
- name: legal
redaction: # replaces defaults.redaction for legal/
enabled: true
rules: legal-redaction.yaml
content:
- path: "legal/*.md"
- path: "src/locales/*.json"
format: json
redaction:
enabled: false # no redaction for this entry
Content that sits in no file, such as the text an MCP tool call passes, is
redacted under defaults.redaction. A .kpz package holds files of every
collection, so in a project that declares a collection's or an item's policy,
kapi exec refuses to run a tool that calls a model over a package; run it on
the project's files instead. A redacted package (below) that
kapi extract --format kpz wrote in this project is the exception: its files
already hold placeholders, so the tool runs and reads them. A flow places its
own redact step, and
kapi refuses to run a flow that sends source to a remote provider without one.
A project flow runs over every file of the run, so it needs the step when any
declaration in the recipe turns redaction on. A built-in flow run on one file
needs it when a policy covers that file.
Where the originals live
| Workflow | Vault |
|---|---|
In-process (secure-translate, custom flows, kapi exec) | In memory on the block; removed after restore, and never written to output |
External (extract --redact → merge) | .kapi/vault/batches/<batch-id>.json, in a directory kept out of version control and written private (0700, files 0600) |
Package from source files (extract <files> -o work.kpz --redact-rules <file> → merge work.kpz) | work.kpz.vault beside the package, written private (0600) |
The vault sits in .kapi/vault/, beside .kapi/work/ and outside it.
Everything under work/ rebuilds, so you can delete it; a withheld original
cannot be rebuilt. The vault is never committed and never synced
to a server, so the only copy is the one on your machine. rm -rf .kapi/work
stays safe at any time; deleting .kapi/ wholesale strands any batch you have
out with a translator, because the merge that would restore the originals has
nothing left to read.
Restoration matches placeholders back to originals by token where the format
preserves inline structure (in-process pipelines and XLIFF), and by the unique
visible token text where the format flattens placeholders to plain text (such
as JSON). On extract, redaction runs before content-memory pre-fill, so
no sensitive value can reach the emitted file through a memory match.
A vault keeps each original under the file it came from, the block, and the
placeholder's token. Block IDs and tokens repeat from file to file, so two files
that each withhold a different name keep both names, and kapi merge gives each
file its own.
A vault written by an earlier kapi records no file, and kapi refuses it. The message names the vault file and what to run:
- For a batch vault under
.kapi/vault/batches/, runkapi extractagain. The new extraction writes a new vault with new files to translate, and a file returned from the old extraction no longer merges. - For the project vault,
.kapi/vault/redaction.json, move the file aside and run the command again. kapi writes a new vault from the content it reads. A recorded edit whose originals are in the old file keeps its placeholders, and the moved file is the only copy of those originals.
A vault written by a newer kapi is refused too; upgrade kapi to read it.
A PO file has no inline codes, so kapi extract --format po writes each
withheld value as its visible placeholder ([REDACTED:Person]), and
kapi merge restores it by that text.
Redacted .kpz packages
Under a redaction policy, or with --redact-rules, kapi extract --format kpz
and kapi extract <files> -o work.kpz write a redacted package. Each source
file in it holds the visible placeholder ([REDACTED:Person]) wherever the
policy withholds a value: the skeleton, the raw source that --with-source
adds, and the content-memory matches and terms that travel with the package. A
match or a term in which the policy's rules find a value stays out of the
package. The package's manifest records that it is redacted and the ID of the
vault that restores it.
The vault never travels inside the package:
kapi extract --format kpzkeeps it in the project, at.kapi/vault/batches/<vault-id>.json, one vault for every package of the run.kapi extract <files> -o work.kpz --redact-rules <file>writes it beside the package, aswork.kpz.vault. Extract never replaces a vault already at that path: merge the package it restores first and remove the vault, or extract to another name.
Whoever receives the package works on the placeholders. A receiver who runs
kapi exec on the package needs the source text in it, so extract it with
--with-source: a package that carries only skeletons gives a tool no text to
work on, and merges empty values. kapi merge restores each file's values,
in every plural form, from the vault whose ID the manifest records: in the
project for a project package, and beside the package for merge work.kpz.
When the vault is not there, or its ID differs, the merge stops before it
writes a file and names the vault it looked for. Keep the placeholders intact
during translation: a translation that still holds a placeholder after the
restore stops the merge, which names the file and the block.
kapi exec runs a tool over a redacted package on the placeholders and never
reads the vault, so the package keeps holding placeholders until the merge. In
a project that declares a collection's or an item's policy, a tool that calls a
model runs over a package only when kapi extract --format kpz wrote the
package in this project, so its vault is among the project's batch vaults.
Extract refuses a file it cannot redact into a package: one in which the
policy's rules still find a value once the blocks are redacted (in its markup,
in a field kapi does not read as a block, or across an inline code), and one
whose block IDs change once the values are withheld. It also refuses a policy
that detects entities, since a .kpz extraction runs no entity-extract step.
A project's XLIFF 2 or PO extraction carries the blocks alone.
Related
- C-10: Content Redaction: the design.
- Flows: transformer steps and the placement pass.
- Bilingual workflow: extract and merge with a translator.