SLOPSHOPPER

config-race-observer

Owned synthetic configuration race observer

newcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · config-race-observer
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /config-race-report ⎿ config-race-observer: ⎿ config-race-observer: CONFIG_RACE_REPORT_1_0_0_ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Redacton

MIT License TypeScript Claude Code Powered by Redact Secret

Keep recognized credentials out of supported Claude Code model inputs. Redacton scans prompt text/context, Read text, and Bash stdout/stderr, replacing detected credentials with placeholders. Recognized private keys block the entire event.

Redacton targets macOS and Linux CLI installations with one shared Mod/helper implementation; Windows has a PowerShell installer candidate but no qualification path. The published verified release remains macOS ARM64, Claude Code 2.1.294, Node 22.16.0. Portable packages, Unix/PowerShell candidate installers and CI are development paths until each platform's actual host is qualified. See the target and verified matrix.

Powered by Redact Secret, a deterministic Rust detection engine with native and WebAssembly runtimes. Scanning runs locally, without sending content to a detection service. Explore Redact Secret for local redaction in your own apps, and star the project to support it.

Install in two minutes

This command installs the existing verified macOS Apple Silicon release, not a universal cross-platform package.

Have Claude Code and Node.js 22 installed on macOS Apple Silicon. The verified combination is Claude Code 2.1.294 and Node 22.16.0; other platforms, host versions and Desktop have not been qualified. Required follow-on support targets macOS x64, Linux x64/ARM64 and Linux x64/ARM64; native Windows and WSL have no qualification path. Those targets are not supported by this installer.

curl -fsSL https://raw.githubusercontent.com/milocosmopolitan/redacton/main/scripts/install.sh | bash
claude --plugin-dir "$HOME/.local/share/redacton/current"

The installer downloads the published package, verifies its pinned checksum, and installs it without npm or a build step. You can inspect the installer or download packages from Releases.

The --plugin-dir option requests loading for that Claude Code launch. Use the same launch command for future sessions. Installed files and a successful helper check do not prove that the Mod is active. Confirm /redacton answers locally with Protect ready; fallback text saying protection is inactive means there is no interception. Cowork remains unsupported, with its activation blocker recorded in host authority.

Linux, Intel Mac, native Windows and WSL users should follow the candidate installation and qualification guidance. Native Windows and WSL are separate environments; Desktop is a separate host surface. A helper/installer success does not establish protection of model inputs.

Use it

After launch, run /redacton and confirm Protect ready before your first task.

New, resumed and branched CLI sessions start with protection requested ON. Readiness is shown separately as loading, ready or unavailable.

CommandEffect
/redactonRequest protection and make one bounded recovery attempt after a local failure.
/redactoffFrom the local terminal, bypass scans for subsequent operations in this session.
/redact:statusShow cached readiness, coverage, rules and recent outcomes.
/redact:config, /redact:add-rule, /redact:remove-ruleManage custom formats through local forms.
/redactconfigOpen the same local configuration form immediately during an active turn.

Commands take no arguments. Never paste secrets, patterns or test text into slash-command arguments; the host can store them before local rejection. Running operations keep the policy they started with. OFF displays a prompt-area warning: ⚠ Redacton OFF — credential protection disabled.

Disabling protection and opening configuration require the host's local composer origin. SDK, plugin, bridge and other origins cannot authorize these actions; /redacton recovery and cached status remain usable. After a transient settings/helper fault, remove its local cause and run /redacton in the same session. Corrupt settings require explicit local repair or scope reset. During recovery selected ON content is withheld and approved rules are retained. See recovery and refusal scope and settings ownership/environment.

Custom rules use guided token formats or assignment names. Validate, inspect synthetic sample outcomes, then explicitly Apply session; personal/project saving and portable import/export are separate actions. Confirm the form has keyboard focus before typing, and close it before ordinary chat. Never enter actual credential values as rule definitions. See configuration and scope.

Know the boundaries

Protection depends on the functioning host and outer guard. Unsupported selected results and scanner failures withhold content under those guard conditions; a host that bypasses all guards can expose it.

Original prompts and tool arguments can remain in host storage. Encoded/base64 credentials can be missed, and zero findings does not mean safe content. Grep text, Glob paths, WebFetch, Write arguments and effects, MCP, other tools, binary/image/audio content, PII and existing history are outside coverage. Withholding output does not undo a tool's effects. The route audit distinguishes these boundaries and links their existing adapter owners.

See compatibility and evidence and the threat model for exact limits. For vulnerabilities, use private security reporting; public issues are not confidential.

Contributing · Security policy · Code of conduct · Dependency notices · MIT license

Source 1 files
hooks/register.js 66 lines
1// The pinned SDK process hook returns { value: ProcessResult }; no output is retained.
2export function helperOutcome(answer, request, elapsedMs, rejected = false) {
3  const value = answer?.value;
4  const result = { outcome: 'PROCESS_SHAPE_INVALID', exitCode: null, elapsedBucket: elapsedMs < 250 ? 0 : elapsedMs < 1000 ? 1 : elapsedMs < 2000 ? 2 : elapsedMs < 5000 ? 3 : 4, stdoutTruncated: value?.isStdoutTruncated === true, stderrTruncated: value?.isStderrTruncated === true, stderrPresent: typeof value?.stderr === 'string' && value.stderr.length > 0, identityMatched: false };
5  if (rejected) { result.outcome = 'PROCESS_REJECTED'; return result; }
6  if (answer?.deny) { result.outcome = 'PROCESS_DENIED'; return result; }
7  if (!value || typeof value !== 'object' || Object.keys(value).sort().join(',') !== 'exitCode,isStderrTruncated,isStdoutTruncated,stderr,stdout' || !Number.isInteger(value.exitCode) || value.exitCode < -2147483648 || value.exitCode > 2147483647 || typeof value.stdout !== 'string' || typeof value.stderr !== 'string' || typeof value.isStdoutTruncated !== 'boolean' || typeof value.isStderrTruncated !== 'boolean') return result;
8  result.exitCode = value.exitCode;
9  if (value.exitCode !== 0) result.outcome = 'PROCESS_EXIT_NONZERO';
10  else if (result.stdoutTruncated) result.outcome = 'STDOUT_TRUNCATED';
11  else if (result.stderrTruncated) result.outcome = 'STDERR_TRUNCATED';
12  else if (result.stderrPresent) result.outcome = 'STDERR_PRESENT';
13  else {
14    if (value.stdout.length > 1048576) return result;
15    let reply;
16    try { reply = JSON.parse(value.stdout); } catch { result.outcome = 'INVALID_JSON'; return result; }
17    result.identityMatched = [1, 2].includes(request?.protocolVersion) && typeof request?.requestId === 'string' && typeof request?.policyId === 'string' && typeof request?.config?.revision === 'string' && reply?.protocolVersion === request.protocolVersion && reply?.requestId === request.requestId && reply?.policyId === request.policyId && reply?.engineVersion === '0.1.0-beta.14' && reply?.configRevision === request.config.revision;
18    result.outcome = reply?.status === 'failed' && reply?.errorCode === 'TIMEOUT' ? 'DECLARED_TIMEOUT' : reply?.status === 'failed' && reply?.errorCode === 'ENGINE_UNAVAILABLE' ? 'DECLARED_ENGINE_UNAVAILABLE' : !result.identityMatched ? 'IDENTITY_MISMATCH' : reply.status === 'ok' ? 'DECLARED_OK' : reply.status === 'blocked' ? 'DECLARED_BLOCKED' : reply.status === 'failed' ? 'DECLARED_FAILED' : 'STATUS_UNKNOWN';
19  }
20  return result;
21}
22
23export function register(on) {
24  const observations = [];
25  const helperOutcomes = [];
26  let reports = 0;
27  let stdoutSanitizes = 0;
28  on('session.start', async ($, e, next) => {
29    await $.command.register({ name: 'config-race-report', description: 'Fixed synthetic captured configuration metadata' });
30    return next(e);
31  });
32  on('process.run', async (_$, e, next) => {
33    let measuredRequest;
34    let measuredIndex = -1;
35    try {
36      const request = JSON.parse(e.init?.stdin ?? '{}');
37      if (request.operation === 'sanitize' && request.segments.some(segment => segment.id === 'stdout')) {
38        stdoutSanitizes = Math.min(3, stdoutSanitizes + 1);
39        if (helperOutcomes.length < 2) {
40          measuredIndex = helperOutcomes.length;
41          measuredRequest = request;
42          helperOutcomes.push(null);
43        }
44        const config = request.config;
45        if (observations.length < 2 && /^cfg-[0-9]{1,6}-[0-9]{1,6}$/.test(config.revision) && Array.isArray(config.rules) && config.rules.length <= 1) {
46          const rule = config.rules[0];
47          const exactRule = !rule || (rule.kind === 'token' && rule.id === 'config-race-token' && rule.prefix === 'syntheticcred_' && rule.alphabet === 'alnum' && rule.run.kind === 'exact' && rule.run.length === 16 && rule.action === 'redact');
48          observations.push({ revision: config.revision, rules: config.rules.length, exactRule });
49        }
50      }
51    } catch {}
52    const started = Date.now();
53    let answer;
54    try { answer = await next(e); }
55    catch (error) {
56      if (measuredIndex !== -1) helperOutcomes[measuredIndex] = helperOutcome(null, measuredRequest, Date.now() - started, true);
57      throw error;
58    }
59    if (measuredIndex !== -1) helperOutcomes[measuredIndex] = helperOutcome(answer, measuredRequest, Date.now() - started);
60    return answer;
61  });
62  on('command.run', { command: 'config-race-report' }, () => ({
63    text: `${helperOutcomes.map((value, index) => value ? `CONFIG_HELPER_${index + 1}_${value.outcome}_${value.exitCode ?? 'N'}_${value.elapsedBucket}_${Number(value.stdoutTruncated)}_${Number(value.stderrTruncated)}_${Number(value.stderrPresent)}_${Number(value.identityMatched)}` : '').join('\n')}\nCONFIG_RACE_REPORT_${++reports}_${stdoutSanitizes}_${observations.length}_${observations.map(value => `${value.revision}_${value.rules}_${value.exactRule ? 1 : 0}`).join('_')}`,
64  }));
65}
66