{"id":48329672,"url":"https://github.com/nrednav/pi-redactor","last_synced_at":"2026-04-05T01:00:15.728Z","repository":{"id":344307860,"uuid":"1178475367","full_name":"nrednav/pi-redactor","owner":"nrednav","description":"Pi extension that redacts sensitive strings from messages before the LLM provider sees them.","archived":false,"fork":false,"pushed_at":"2026-03-14T04:08:49.000Z","size":96,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-14T14:09:20.247Z","etag":null,"topics":["llm","pi-extension","pi-package","privacy","redaction","redactor","security"],"latest_commit_sha":null,"homepage":"https://nrednav.github.io/pi-redactor","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nrednav.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-11T03:56:14.000Z","updated_at":"2026-03-14T04:42:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nrednav/pi-redactor","commit_stats":null,"previous_names":["nrednav/pi-redactor"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/nrednav/pi-redactor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrednav%2Fpi-redactor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrednav%2Fpi-redactor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrednav%2Fpi-redactor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrednav%2Fpi-redactor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nrednav","download_url":"https://codeload.github.com/nrednav/pi-redactor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrednav%2Fpi-redactor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31420789,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T00:25:07.052Z","status":"ssl_error","status_checked_at":"2026-04-05T00:25:05.923Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["llm","pi-extension","pi-package","privacy","redaction","redactor","security"],"created_at":"2026-04-05T01:00:12.720Z","updated_at":"2026-04-05T01:00:15.718Z","avatar_url":"https://github.com/nrednav.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pi-redactor\n\nPi extension that redacts sensitive strings from user input, tool results, and context before the LLM sees them.\n\n## Installation\n\n```bash\npi install npm:pi-redactor\n```\n\nOr from a local path:\n\n```bash\npi install /path/to/pi-redactor\n```\n\n## How It Works\n\nThe extension intercepts messages at three points:\n\n1. **`input`** — rewrites your message before it reaches the LLM.\n2. **`tool_result`** — redacts tool output (file reads, command output, etc.) before it enters the conversation.\n3. **`context`** — scans the full message history immediately before each LLM call, catching secrets in historical context, assistant echoes, or custom messages.\n\nAll configured patterns are applied using a single-pass regex with longest-match-first semantics. The transformed text is sent instead of the original.\n\nThe LLM never sees the original sensitive strings.\n\nIf redaction fails at runtime (e.g., a malformed pattern), the extension **fails closed**: user messages are blocked, tool results are replaced with an error, and context is emptied. An error notification is displayed in each case. Disable the redactor with `/redact off` to bypass.\n\n## Commands\n\n| Command | Description |\n|---------|-------------|\n| `/redact` | Show help and usage |\n| `/redact add \u003cstring\u003e` | Add pattern; replaces with `[REDACTED]` |\n| `/redact add \u003cstring\u003e as \u003clabel\u003e` | Add pattern with custom replacement label |\n| `/redact remove \u003cstring\u003e` | Remove pattern by its original string |\n| `/redact list` | Show all active patterns |\n| `/redact clear` | Remove all patterns (prompts for confirmation) |\n| `/redact on` | Enable redaction |\n| `/redact off` | Disable redaction |\n| `/redact limit \u003cn\u003e` | Set max pattern count (default 100, max 1000) |\n\n### Parsing Rule\n\nThe delimiter ` as ` splits original from label. The **last** occurrence of ` as ` is used, so:\n\n```\n/redact add John Smith as CEO as [PERSON]\n```\n\nRedacts `John Smith as CEO` → `[PERSON]`.\n\n## Example\n\n```\n/redact add John Smith as [CLIENT]\n/redact add acct_12345\n```\n\nInput: `\"Contact John Smith about acct_12345.\"`\n\nLLM receives: `\"Contact [CLIENT] about [REDACTED].\"`\n\nNotification: `🔒 Redacted 2 occurrence(s) from your message`\n\n## Behavior\n\n- **Case-insensitive:** `john smith` matches `John Smith`.\n- **Longest match wins:** If both `secret` and `secretkey` are patterns, the input `secretkey` matches the longer pattern. Patterns do not apply sequentially.\n- **Match order is deterministic:** Results are identical regardless of the order patterns were added.\n- **Fail-closed on error:** If redaction fails, the content is blocked — it never reaches the LLM unredacted. User messages return `handled`, tool results are replaced with an error, and context is emptied.\n- **Disabled on degraded config:** If the config file is corrupted or patterns could not be loaded, the redactor starts disabled. Run `/redact list` to review, then `/redact on` to re-enable.\n- **Default limit: 100 patterns.** Configurable up to 1000 via `/redact limit \u003cn\u003e`. Adding a pattern beyond the current limit produces an error.\n- **Maximum 1000 characters** per pattern (original and replacement).\n- **Stale pattern warnings.** At session start, a warning is shown if any patterns were added more than 90 days ago.\n\n## Status Bar\n\n| State | Display |\n|-------|---------|\n| Enabled, N patterns (N \u003e 0) | `🔒 Redactor: N pattern(s)` |\n| Enabled, 0 patterns | `🔒 Redactor: no patterns` |\n| Disabled | `🔓 Redactor: off` |\n\n## Configuration\n\nPatterns persist to a platform-specific state directory that is outside of typical version-controlled dotfile paths:\n\n| Platform | Path |\n|----------|------|\n| Linux | `~/.local/state/pi-redactor/config.json` |\n| macOS | `~/.local/state/pi-redactor/config.json` |\n| Windows | `%LOCALAPPDATA%\\pi-redactor\\config.json` |\n\nThe `XDG_STATE_HOME` environment variable is respected on Linux/macOS. On Windows, `LOCALAPPDATA` is used.\n\n### On-Disk Format\n\nThe file uses an envelope format with integrity checking:\n\n```json\n{\n  \"version\": 1,\n  \"configVersion\": 3,\n  \"checksum\": \"\u003csha256-hex-of-serialized-data\u003e\",\n  \"data\": {\n    \"enabled\": true,\n    \"maxPatterns\": 100,\n    \"patterns\": [\n      {\n        \"original\": \"John Smith\",\n        \"replacement\": \"[CLIENT]\",\n        \"createdAt\": 1710000000000\n      }\n    ]\n  }\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `version` | Envelope schema version. Currently `1`. |\n| `configVersion` | Monotonically incrementing counter for concurrency control. |\n| `checksum` | SHA-256 hex digest of `JSON.stringify(data)`. Used for tamper detection. |\n| `data.enabled` | Whether redaction is active. |\n| `data.maxPatterns` | Maximum number of patterns allowed. Default `100`, max `1000`. |\n| `data.patterns[].original` | The original string to redact. |\n| `data.patterns[].replacement` | The replacement label. |\n| `data.patterns[].createdAt` | Unix timestamp in milliseconds when the pattern was added. |\n\n### Integrity and Concurrency\n\n- **Checksum verification.** On load, the checksum is recomputed and compared. A mismatch triggers a tamper warning and disables the redactor until the user reviews.\n- **Optimistic concurrency control.** Each save increments `configVersion`. If another pi session modified the file since this session last read it, the save fails with an error instructing the user to reload.\n- **Atomic writes.** Saves write to a temporary file and rename, preventing corruption from interrupted writes.\n- **Degraded config recovery.** If the config file is corrupted, unreadable, or has dropped patterns, the extension starts with the redactor disabled and displays a warning. The user must explicitly re-enable after reviewing.\n\n## Limitations\n\n- No expiration or automatic cleanup of stored patterns.\n- Performance may degrade as pattern count approaches the 1000 ceiling due to regex alternation size. Untested above 50 patterns at maximum character length.\n- `/redact list` displays the raw original strings in the local UI. These are the sensitive values being redacted. Do not use this command where the terminal output may be captured or shared.\n- Pattern matching uses `toLowerCase()` without Unicode normalization. Visually identical strings in different normal forms (NFC vs NFD) are treated as distinct patterns (e.g., `é` as U+00E9 vs U+0065 U+0301).\n- **Signed thinking blocks are not redacted.** The `context` handler scans unsigned thinking blocks, but blocks with a `thinkingSignature` are passed through unchanged. Modifying the thinking text would invalidate the cryptographic signature used by LLM providers for multi-turn thought chain continuity. Secrets that appeared in signed thinking blocks (from turns before a redaction pattern was configured) will persist in the conversation context.\n- **Redacted thinking blocks are opaque.** Blocks with `redacted: true` are encrypted payloads with no meaningful text to scan.\n- **ToolCall arguments are not redacted.** The `context` handler does not scan `type: \"toolCall\"` block arguments. The corresponding `ToolResultMessage` content is redacted.\n- **TOCTOU window in config saves.** Concurrent saves within milliseconds from separate processes can silently overwrite each other. This is bounded by the single-process Pi event model and is not expected to occur in normal usage.\n\n## Development Disclosure\n\nThis package was developed predominantly using an AI coding agent.\nAll code, tests, and documentation were reviewed, validated, and approved by me.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrednav%2Fpi-redactor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnrednav%2Fpi-redactor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrednav%2Fpi-redactor/lists"}