SLOPSHOPPER

redact-restore-probe

Test-only probe: logs booleans about raw model answers

newprompt
v0.0.1AGPL-3.0updated 2026-10-12isandrel/redact-restore/tests/e2e/probe
A shopper browsing a rack in a slop shop
README

๐Ÿ” Redact Restore

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.

CI License: AGPL-3.0 Claude Code Mod Status: experimental

/plugin install redact-restore --marketplace isandrel/redact-restore

๐Ÿงช Try it in 2 minutes with a fake email


โœจ Why Redact Restore

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:

  • ๐Ÿ›ก๏ธ The model sees placeholders, such as {{PRIVATE:EMAIL#3fa1c09e8b2d}}, never the value.
  • ๐Ÿ” You see the real thing. Messages render with the original values on your screen.
  • ๐Ÿงฐ Your tools still work. Bash, Edit, Write, Grep and local MCP tools receive the real values, so builds, scripts and file edits are correct.
  • ๐Ÿ”‘ Nothing changes about your login. Official Claude Pro/Max OAuth, no API gateway, no proxy, no custom base URL.
  • ๐Ÿ’ธ Free and open source (AGPL-3.0). No account, service or fee of its own: you only need the Claude Code you already use, on whatever Claude plan runs it.

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.

๐Ÿงช Try It in 2 Minutes

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:

WhereYou should seeWhich proves
The reply on your screenalice@example.comPlaceholders 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 terminalalice@example.comLocal tools receive the original
/redact-restore sessionOne masked value of kind EMAIL, with its placeholderWhat 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.

๐Ÿงญ How It Works

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

Everything that leaves is masked, then audited

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"]

Local tools get real values; model-bound tools never do

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"]

๐Ÿš€ Features

FeatureWhat it does
๐ŸŽฏ Your own valuesList 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+ detectors200+ 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 depthMasks prompts, context, attachments, the system prompt and every stored row; audits each request before it is sent; withholds images it cannot scan.
๐Ÿ” Reversible by designSame 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 revealedThe 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.
โš™๏ธ ConfigurablePlaceholder 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.
๐Ÿ“Š VisibleA 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).
๐Ÿงช TestedUnit 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.

๐Ÿ†š Compared With

CapabilityAPI gateway or proxyOne-way redaction hooksRedact 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โŒโœ…

๐Ÿ“ฆ Quick Start

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.
  • For your most sensitive values, use the keychain instead: /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 local limits it to one folder.

๐Ÿ“š Documentation

DocumentWhat it covers
ConfigurationEvery option, placeholder formats, labels and aliases, status fields
DetectorsWhat is detected, how it is validated, how to turn detectors on or off
Security modelCoverage of every path to the model, review findings, known limitations
CompatibilityCLI and Desktop Code test matrix
Test planAutomated and live test steps
ChangelogRelease notes

โš ๏ธ Status

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.

๐Ÿค Contributing

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.

๐ŸŒ Community

Thanks to LINUX DO for supporting open-source developers and community sharing.

๐Ÿ“„ License

GNU Affero General Public License v3.0. The vendored gitleaks rules in hooks/vendor/ are MIT-licensed; see hooks/vendor/LICENSE-gitleaks.txt.

Source 1 files
hooks/register.ts 47 lines
1import 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