Test-only probe: logs booleans about raw model answers

Redacted for the model. Restored for you.
Use Claude Code on private projects without handing your secrets to the model.<br> Private values become placeholders before they leave your machine, and come back the moment they return.
/plugin install redact-restore --marketplace isandrel/redact-restore
๐งช Try it in 2 minutes with a fake email
Every prompt you type, every file Claude reads and every command output goes to the model. Customer names, internal hostnames, API keys, the email in your git config: all of it is context.
Redact Restore sits inside Claude Code as a native Mod:
{{PRIVATE:EMAIL#3fa1c09e8b2d}}, never the value.Bash, Edit, Write, Grep and local MCP tools receive the real values, so builds, scripts and file edits are correct.What it does not do. It is experimental, not a data-loss-prevention product. It hides only values it knows: your rules plus the built-in detectors. Images and PDFs are withheld, not scanned. Local tools and other installed mods see real values by design. Details: security model.
No setup and no real data: email detection is on by default, so one fake address shows the whole round trip.
1. Install at a Claude Code prompt, then start a new session:
/plugin install redact-restore --marketplace isandrel/redact-restore
2. Send this prompt:
My test email is alice@example.com. Spell it back one character at a time, then run: echo alice@example.com > redact-test.txt
3. Check the result:
| Where | You should see | Which proves |
|---|---|---|
| The reply on your screen | alice@example.com | Placeholders are restored locally |
| The spelled-out characters | { { P R I V A T E : E M A I L # โฆ | The model only ever had the placeholder |
cat redact-test.txt in your own terminal | alice@example.com | Local tools receive the original |
/redact-restore session | One masked value of kind EMAIL, with its placeholder | What was masked this session |
4. Tell us what happened. Share what you saw in each row, and anything masked that should not be, through the bug report or false positive forms. Use fake data only.
sequenceDiagram
autonumber
actor You
participant RR as Redact Restore<br/>(on your machine)
participant Model as Claude model API
participant Tools as Local tools
You->>RR: Email alice@example.com about Project Phoenix
RR->>Model: Email {{PRIVATE:EMAIL#35;3fa1โฆ}} about {{PRIVATE:CUSTOM#35;9c2eโฆ}}
Model-->>RR: Run a command with {{PRIVATE:EMAIL#35;3fa1โฆ}}
RR->>Tools: Command runs with alice@example.com
Tools-->>RR: Output (may contain originals)
RR->>Model: Output masked again
Model-->>RR: Reply with placeholders
RR-->>You: Reply shown with the original values
flowchart LR
subgraph Local["๐ฅ๏ธ Your machine"]
P[Your prompts]
C[CLAUDE.md and account email]
A[Mentioned files and reminders]
S[System prompt]
R[Tool results]
M{{"Mask<br/>your values ยท email ยท 240+ detectors"}}
AU{"Pre-send audit"}
P --> M
C --> M
A --> M
S --> M
R --> M
M --> AU
end
AU -->|placeholders only| API[("Claude API")]
AU -.->|value still visible| X["โ Request refused"]
flowchart TD
Call["Tool call from the model"] --> Kind{"Where does its input go?"}
Kind -->|"To another model or service:<br/>Agent, WebFetch, WebSearch, Workflow, โฆ"| Keep["Placeholders kept"]
Kind -->|"Stays on this machine:<br/>Bash, Edit, Write, Grep, local MCP"| Mode{"restore_mode"}
Mode -->|"all (default)"| Real["Original values restored"]
Mode -->|"files"| Files["Only file edits restored"]
Mode -->|"none"| Refuse["Placeholders kept; file edits refused"]
| Feature | What it does |
|---|---|
| ๐ฏ Your own values | List names, codenames, hosts or any exact string. In a form: pick a label (PROJECT โ {{PRIVATE:PROJECT#โฆ}}) or set a fixed alias (CODENAME_X). Add patterns for formats such as ACME-[A-Z0-9]{16}, checked locally before they run. |
| ๐ 240+ detectors | 200+ API key and token formats (Anthropic, OpenAI, AWS, GitHub, Stripe, Google, โฆ, including the gitleaks rule set), JWTs, private keys, passwords, KEY=value settings, internal host names, IP and MAC addresses, cards, IBANs, and Chinese ID, mobile and company codes, with checksum and entropy validation. |
| ๐งฑ Defense in depth | Masks prompts, context, attachments, the system prompt and every stored row; audits each request before it is sent; withholds images it cannot scan. |
| ๐ Reversible by design | Same value, same placeholder within a session (or in every session with placeholder_ids: stable), kept across /resume. Restored on screen (messages, tool rows, while streaming) and in local tool inputs. Unknown placeholders are refused, never guessed. |
| ๐ท๏ธ Described, not revealed | The model learns what a value is (client company name, the value of DB_PASSWORD) without seeing it, so it can still reason about the task. |
| โ๏ธ Configurable | Placeholder format, which tools get originals, which detectors run, status line content: all options, all with safe defaults. |
| ๐๏ธ Rules pane and scan | /redact-restore rules to add, update or remove values; /redact-restore scan [PATH] to find your name, coworkers, hosts and settings values locally, on this machine or in any folder; import and export to move them between machines. Values are shown only on your screen, masked until you ask. |
| ๐ Visible | A status line such as 4 private values masked, a Session tab with this session's counts and placeholders, a template, and a status file your own statusLine script can read (counts only, never values). |
| ๐งช Tested | Unit and integration suites through the engine's own dispatch, live tests in a real Claude Code session, and a regression test for every reviewed leak. |
| Capability | API gateway or proxy | One-way redaction hooks | Redact Restore |
|---|---|---|---|
| Keeps the official Claude login and endpoint | โ | โ | โ |
| Model never sees configured values | โ | โ | โ |
| Shows you the original values | โ | โ | โ |
| Local commands and file edits get real values | โ | โ ๏ธ some | โ |
| Model-bound tools kept on placeholders | โ | โ | โ |
| Checks every request before it is sent | โ ๏ธ varies | โ | โ |
1. Install from isandrel/redact-restore. At a Claude Code prompt, in the terminal or the desktop app's Code tab (answer y to add the marketplace, then pick a scope):
/plugin install redact-restore --marketplace isandrel/redact-restore
Or from a shell:
claude plugin marketplace add isandrel/redact-restore
claude plugin install redact-restore@redact-restore
2. Use Claude as usual. Paste logs, configs or snippets with tokens and secrets: emails, API keys, passwords, private keys, KEY=value settings, card, phone and ID numbers are masked before they leave and restored on your screen.
3. Add what detectors cannot know, such as names, customers and codenames. Unsure about a piece of text? /redact-restore test shows what the model would receive, on your machine, before you send it:
/redact-restore scan finds your git identity, coworkers, remotes, machine and account names, and .env values; /redact-restore scan PATH checks any folder or file. It runs on your machine, so nothing is sent to the model to judge. Pick Add or Ignore in the pane./redact-restore rules adds your own in a form: the private value (Acme Corp), then a label (CUSTOMER) or a fixed alias (CLIENT_A), with a preview of what the model sees./plugin โ Installed โ redact-restore โ Configure options โ Exact private values.4. Start clean and check. Run /clear, then /redact-restore session or look at the status line under the prompt. Ask Claude to spell a protected value character by character: it can only spell the placeholder.
Update later with claude plugin update redact-restore@redact-restore. The plugin also ships skills you can ask for by name: /redact-restore:protect, :scan, :status, :policy, :placeholders, :data, :troubleshoot, :import and :export.
Requires Claude Code 2.1.287 or newer with Mods support (the desktop app's Code tab from 2.1.286). A user-scope install is active in every CLI and Desktop Code session;
--scope locallimits it to one folder.
| Document | What it covers |
|---|---|
| Configuration | Every option, placeholder formats, labels and aliases, status fields |
| Detectors | What is detected, how it is validated, how to turn detectors on or off |
| Security model | Coverage of every path to the model, review findings, known limitations |
| Compatibility | CLI and Desktop Code test matrix |
| Test plan | Automated and live test steps |
| Changelog | Release notes |
Redact Restore is experimental. It is tested live in the Claude Code CLI (2.1.287, and 2.1.292 for display restore, descriptions, /resume and the prompt cache), including subagents, compaction and concurrent sessions. In the desktop app's Code tab the status line is confirmed; the rest is kit-tested only so far. It is not a data-loss-prevention product: it protects the paths described in the security model, and tools that run locally act on real values by design. Try it with synthetic data first.
Contributions are welcome: new detectors, false-positive fixes, docs and tests. Read the contributing guide, and report leaks privately through SECURITY.md, never in a public issue. This project follows a Code of Conduct.
Thanks to LINUX DO for supporting open-source developers and community sharing.
GNU Affero General Public License v3.0. The vendored gitleaks rules in hooks/vendor/ are MIT-licensed; see hooks/vendor/LICENSE-gitleaks.txt.
hooks/register.ts 47 lines1import type { Register } from 'claude-code';
2
3const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
4// Placeholders in the default format ({{PRIVATE:<LABEL>#<12 hex>}}).
5const TOKEN = /\{\{PRIVATE:[A-Za-z][A-Za-z0-9_]{0,31}#[a-f0-9]{12}\}\}/g;
6
7// Test-only. Logs booleans and counts about raw model answers; never text or values.
8/** The id's first six characters: enough to tell placeholders apart in a log, never a value. */
9const short = (token: string): string => token.slice(-14, -8);
10
11export const register: Register = on => {
12 on('prompt.attachment', async ($, e, next) => {
13 const result = await next(e);
14 const tokens = [...new Set((result.text ?? '').match(TOKEN) ?? [])];
15 if (tokens.length > 0) {
16 const held = await ($.state.get as (ref: unknown) => Promise<{ value?: Record<string, string> }>)({
17 plugin: 'redact-restore',
18 key: 'vault',
19 });
20 const vault = held.value ?? {};
21 $.ui.log(`PROBE-ATT ${e.type} tokens=${tokens.map(t => short(t) + (t in vault ? '+' : '-')).join(',')}`);
22 }
23 return result;
24 });
25
26 on('turn.step', async function* ($, e, next) {
27 const result = yield* next(e);
28 const tokens = [...new Set(result.answer.match(TOKEN) ?? [])];
29 const held = await ($.state.get as (ref: unknown) => Promise<{ value?: Record<string, string> }>)({
30 plugin: 'redact-restore',
31 key: 'vault',
32 });
33 const vault = held.value ?? {};
34 const known = tokens.filter(token => token in vault).length;
35 // Synthetic test values, `|`-separated, from the test's environment: counted, never logged.
36 const probes = ((await $.env.get('REDACT_RESTORE_PROBE_VALUES')) ?? '').split('|').filter(Boolean);
37 const raw = probes.filter(value => result.answer.includes(value)).length;
38 $.ui.log(
39 `PROBE agent=${e.agentId ?? 'main'} placeholder=${tokens.length > 0} rawEmail=${EMAIL.test(result.answer)} ` +
40 `rawValues=${raw}/${probes.length} ` +
41 `tokensInVault=${known}/${tokens.length} vaultSize=${Object.keys(vault).length} ` +
42 `answer=${tokens.map(short).join(',')} vault=${Object.keys(vault).map(short).join(',')}`,
43 );
44 return result;
45 });
46};
47