omp packages

@zxh126/pi-redact-secrets0.2.0

Redact secrets from pi tool output, user input and messages before they reach the model or the session file.

zxh126 Published 5 hours ago MIT ~212 downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

pi-redact-secrets

English · 简体中文

在内容进入模型上下文或写入会话文件之前把敏感串替换掉。

一个 pi 扩展,挂在文本进入对话的三条路径上:

钩子 覆盖范围
input 你输入/粘贴的内容——包括从 QQ、Telegram、Slack 等桥接进来的消息
tool_result 所有工具输出:bash、pwsh、read、grep、MCP、codemode 脚本
message_end 模型自己的输出,避免它把密钥复述回对话里

因为脱敏发生在入口处,密钥不会进入模型上下文,也不会写进 ~/.pi/agent/sessions/*.jsonl。

你 / 工具 ──▶ [ 脱敏 ] ──▶ 模型上下文 ──▶ 会话文件
                 │
                 └─ 规则(已知形状) + 字面量(你机器上的真实值)

安装

pi install npm:@zxh126/pi-redact-secrets

只想临时试用、不动配置:

pi -e npm:@zxh126/pi-redact-secrets

然后在 pi 里 /reload(或新开),查看状态:

/redact

配置

全部可选。没有配置文件时使用标准规则集、没有字面量。

/redact init       # 生成 ~/.pi/agent/redact-secrets.json
{
  "redactInput": true,
  "redactToolResult": true,
  "redactMessage": true,
  "profile": "standard",          // "standard" | "strict"
  "secrets": ["我的精确口令"],
  "secretFiles": ["~/.env", "~/.pi/agent/dbhub/mysql.toml"],
  "secretCommand": {
    "command": "pwsh",
    "args": ["-NoProfile", "-File", "~/.pi/agent/dump-secrets.ps1"],
    "timeoutMs": 8000
  },
  "minSecretLength": 8,
  "logFile": "~/.pi/agent/redact-secrets.log"
}

环境变量 PI_REDACT_SECRETS_CONFIG 可以指定别的配置路径。

键 默认 说明
redactInput true 是否脱敏用户输入。若有时确实要把口令交给模型,可关掉
redactToolResult true 是否脱敏工具输出
redactMessage true 是否脱敏最终消息
profile "standard" strict 额外启用高熵规则,能抓未知形状的随机串,但 base64 也会被误伤
secrets [] 逐字替换的字面量
secretFiles [] 从文件提取字面量:.env/TOML/YAML 里 key 名像密钥的 KEY=value、scheme://user:pass@ 形式的 DSN、以及含密钥字段的 .json
secretCommand — 执行该程序,stdout 输出 JSON 字符串数组或每行一个值作为字面量;不经 shell,有超时
minSecretLength 8 太短的字面量会被忽略(否则会误伤普通文本)
logFile ~/.pi/agent/redact-secrets.log 每次脱敏记一行:按规则的命中数,不含值

为什么需要字面量

规则只能抓「有特征的」密钥(sk-…、ghp_…、PEM 块、DSN、password=…)。 像 264613@Ms 这种数据库口令在文本里看起来就是普通词,规则抓不到。 字面量补这个缺口:把 secretFiles 指向你机器上已有的配置文件,或用 secretCommand 在会话启动时现算。

扩展本体不带任何加密实现:它只在会话启动时读一次明文并保存在内存里。加密交给你 本来就信任的那个密钥库,按平台来:

平台 密钥库 对应命令
Windows DPAPI(CryptProtectData) pwsh -File dump-secrets.ps1
macOS 钥匙串 security find-generic-password -s pi-secrets -w
Linux(桌面) Secret Service(GNOME Keyring / KWallet) secret-tool lookup service pi-secrets
Linux(服务器) 0600 文件,或 systemd ≥ 250 上的 systemd-creds cat ~/.pi-secrets / systemd-creds decrypt x.cred -

选哪个之前得知道两条限制,因为 secretCommand 是没有 TTY 的后台执行:

  • 必须能无提示解锁。DPAPI、钥匙串、Secret Service 都满足:系统在登录时就已经拿到了密钥。 gpg、age、pass 不满足:它们通过 pinentry 要口令,后台命令直接失败 (除非 agent 已解锁)。真想用它们就写 --pinentry-mode loopback --passphrase-file, 或者包一层从密钥环取口令的脚本。
  • 耐久性。用户级 DPAPI 密文绑在用户配置上。你自己改密码时 Windows 会重新包装主密钥, 但管理员重置密码(或重建用户配置)后主密钥就解不开了——密钥数据直接报废, Windows 凭据管理器里的东西(git 凭据、RDP 密码)一起没。要么做一张密码重设盘, 要么把长期密钥放到不依赖账户的地方(密码管理器、硬件令牌、带 TPM 的 systemd-creds)。 这其实是所有「绑定登录态」的密钥库共有的问题,钥匙串和 Secret Service 也一样—— 只是 DPAPI 这条翻车声音最大。

每个都只是一个打印值的可执行文件,所以 secretCommand 本身保持平台无关:

// Windows:DPAPI blob,由 PowerShell 解密
{ "secretCommand": { "command": "pwsh", "args": ["-NoProfile", "-File", "~/dump-secrets.ps1"] } }

// macOS:钥匙串里一条,每行一个值
{ "secretCommand": { "command": "security", "args": ["find-generic-password", "-s", "pi-secrets", "-w"] } }

// Linux:Secret Service
{ "secretCommand": { "command": "secret-tool", "args": ["lookup", "service", "pi-secrets"] } }

// Linux 没有密钥环:一个只有你账户能读的文件
{ "secretCommand": { "command": "cat", "args": ["/home/me/.pi-secrets"] } }

命令可以输出 JSON 字符串数组,也可以输出纯文本、每行一个(secret-tool、 security、pass、gpg 都是这种形式)。需要管道就用显式 shell,因为 secretCommand 从不经 shell:{ "command": "sh", "args": ["-c", "pass show pi/secrets"] }。

Windows DPAPI 示例(dump-secrets.ps1,输出 JSON 数组):

$list = [System.Collections.Generic.List[string]]::new()
function AddV($v) {
  if (-not $v) { return }
  $s = [string]$v
  if ($s.Length -ge 8 -and $s -notmatch '^\d+$') { if (-not $list.Contains($s)) { $list.Add($s) } }
}
# …在这里读你自己的配置文件或解密 DPAPI blob…
AddV (ConvertTo-SecureString (Get-Content "$HOME\.secret.dpapi" -Raw) |
  ForEach-Object { [Runtime.InteropServices.Marshal]::PtrToStringBSTR(
    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($_)) })
ConvertTo-Json -InputObject @($list) -Compress

Linux 上无密钥环时的等价做法是一个 chmod 600 文件;要加密又不能提示, 就用 systemd-creds(systemd ≥ 250)或 TPM 支持的存储:

printf '%s\n' "$DB_PASSWORD" "$SMTP_CODE" > ~/.pi-secrets
chmod 600 ~/.pi-secrets
# secretCommand = { command = "cat", args = ["/home/me/.pi-secrets"] }

secretFiles 是零配置选项:直接用你已经有的 chmod 600 文件(.env、类 ~/.pgpass 的配置、dbhub/mysql.toml)。它不提供加密,只有文件系统权限——单用户笔记本尚可, 共享主机不够。

规则

规则 输入示例 输出
pem -----BEGIN OPENSSH PRIVATE KEY-----… <REDACTED-PEM>
auth-header Authorization: Bearer abcdef123456 Authorization: Bearer <REDACTED>
openai-key sk-proj-abcdefghijklmnop sk-<REDACTED>
github-token ghp_abcdefghijklmnopqrst <REDACTED-GH>
npm-token npm_abcdefghijklmnopqrstuvwxyz npm_<REDACTED>
aws-access-key AKIAIOSFODNN7EXAMPLE AKIA<REDACTED>
jwt eyJhbGciOi….eyJzdWIi….s3cr3t <REDACTED-JWT>
dsn mysql://root:pa55word@host/db mysql://root:***@host/db
url-userinfo https://user:pass@host https://***:***@host
json-secret {"appSecret": "eFh2DD4o…"} {"appSecret": "<REDACTED>"}
key-value SMTP_PASS=…、db_password: …、密码:… SMTP_PASS=<REDACTED>
high-entropy(strict) 长随机串 <REDACTED-HIGH-ENTROPY>

规则是幂等的:已脱敏的值(***、<REDACTED>)不会被二次匹配,恢复会话时 不会出现占位符套占位符。

命令

命令 作用
/redact 规则数、字面量数量与来源、开关、配置与日志路径
/redact reload 重新读取配置并重跑 secretCommand
/redact init 写一份示例配置(不会覆盖已有文件)

跨平台

纯 Node 实现:没有原生模块、没有 shell 调用、没有平台分支。

平台 状态
Windows 10/11 已验证:Node 24、PowerShell 7、pi 1.4
Linux (x64) 已验证:Ubuntu 20.04 + Node 22.23.4(28 个测试,含钩子接线测试)
macOS / arm64 预期可用——与 Linux 同一套代码路径
  • 需要 Node ≥ 22.18:包直接发布 TypeScript 源码,依赖 Node 原生类型剥离(pi 本身也要求 Node 22+)。
  • 路径一律走 path.join / os.homedir(),配置值里的 ~、~/ 会被展开;代码里没有任何盘符或 Windows 专有 API。
  • secretCommand 通过 execFile 执行,不经 shell,命令与参数由你按平台填写(Windows 用 pwsh,Linux 用 sh/node/python3),没有任何写死的可执行文件。
  • 日志文件在 POSIX 上以 0600 权限创建(Windows 忽略该模式)。

它不做什么

  • 不脱敏工具参数。改写 bash/pwsh 的参数等于改变实际执行的命令,所以 pwsh -Command "login --password hunter2" 里的口令仍会留在工具调用里。 请改用环境变量或文件传参。
  • 不动 tool_result.details,TUI 渲染依赖它。
  • 脱敏是「尽力而为 + 基于模式」的:它降低暴露面,不是保证。最安全的做法仍然是 让敏感数据不离开本机。
  • 已经写进会话文件的历史不会自动清理。需要的话对 ~/.pi/agent/sessions/**/*.jsonl 做一次离线处理。

开发

npm test          # node --test,无需构建(Node ≥ 22.18 原生剥离类型)
npm run check     # tsc --noEmit
pi -e .           # 从本目录加载包

CI 在 ubuntu-latest 上用 Node 22 和 24 跑 npm test 与 npm run check。 test/extension.test.ts 用一个假的 ExtensionAPI 驱动扩展,不用启动 pi 就能覆盖三个钩子、 配置读取、字面量收集和日志文件。

结构:

src/index.ts     pi 接线:三个钩子 + /redact 命令
src/rules.ts     规则与替换模板
src/redactor.ts  纯脱敏引擎(有单测、无 I/O)
src/literals.ts  字面量收集(配置、文件、命令)
src/config.ts    配置结构、默认值与校验

引擎不依赖 pi,可以单独使用:

import { Redactor } from "@zxh126/pi-redact-secrets/src/redactor.ts";

const redactor = new Redactor({ profile: "strict" });
redactor.setSecrets(["我的精确口令"]);
redactor.redact("password=我的精确口令");
// { text: "password=<REDACTED>", hits: 1, byRule: {…} }

存储与引用(0.2.0 起)

0.2.0 把单独的 pi-secret-ref 并入本包:存值、注入、引用、脱敏共用同一份内存里的值。不再需要装两个扩展,也不再有加载顺序问题。

命令

命令 作用
/secret db=<值> 存一个值。打字时可见,提交后转录里只有 db=******
/secret db 同上,值从输入框取——不进编辑器、不进 ↑ 历史,推荐
/secret 列出 key 名、注入的环境变量名、后端(永不显示值)
/secret test db 验证能解密并已注入,只报长度
/secret rm db 删除,并还原该环境变量
/secret import db --from <来源> env:NAME / dpapi:<path> / <文件> / command
/secret reload 重读配置、字面量来源和密钥库
/redact 规则数、字面量数、存储的 key 数、后端、加载时间
/redact reload / /redact init 重读全部 / 写示例配置

引用:四种写法,等价

[$secret.db]   [$secret:db]   <secret.db>   <secret:db>

key 名大小写不敏感。两种展开策略:

  • A 环境变量(默认):写在你的消息里,展开成 $env:PI_SECRET_DB(bash 下 $PI_SECRET_DB)。模型只看到变量名,值在子进程里展开。
  • B 执行期替换(opt-in,白名单):写在 pwsh / bash 的工具参数里,执行前换成真值,transcript 留下占位符。要把值写进文件就显式把 write / edit 加进白名单——那已经超出"不进上下文"的范围。

未知 key 两条路都失败关闭:A 原样保留并提示;B 直接 block 工具调用,绝不当空串放过去。

注意:因为 B 对白名单工具做整串扫描,任何文档、代码、脚本里出现引用写法(哪怕是举例)都会被当成引用处理;key 不存在就直接 block。写文档时用 secret.key 之类的变体描述,或者把示例放在不经过 shell 的工具里。确实需要放宽时设 injection.substitution.blockOnUnknown: false:未知 key 会原样保留,不再阻断调用(失败会推迟到命令本身)。

后端

平台 后端 位置
Windows DPAPI(当前用户) store/<key>.dpapi
macOS Keychain(service pi-redact-secrets) 系统钥匙串
Linux Secret Service(secret-tool) 系统密钥环
兜底 仅文件权限,不加密 store/<key>.secret(0600)

store/index.json 只记 {backend, updated, length};值只在后端里。启动时一次 pwsh 批量解密所有 DPAPI blob,不是每个 key 一次。

配置新增两段

{
  "store": { "dir": "~/.pi/agent/secrets/store", "backend": "auto" },
  "injection": {
    "envPrefix": "PI_SECRET_",
    "exposeKeyNames": true,
    "envRefSyntax": "pwsh",
    "substitution": { "enabled": true, "tools": ["pwsh", "bash"], "blockOnUnknown": true }
  }
}

exposeKeyNames 把 key 名(不含值)写进 system prompt 的 secrets 段。store.backend 可强制 file(CI / 无密钥环时最省事)。老的配置键一个没变,不写这两段就是默认行为。

边界

  • 模型知道变量名,就能自己 echo $env:PI_SECRET_DB。工具输出里的已知值会被脱敏,但脱敏是模式匹配——base64、取子串这类变体会漏。
  • 环境变量对子进程透明:MCP server、嵌套 pi、PTY 都能读到。高价值凭据应该用短期凭据。
  • /secret db=值 会进编辑器的内存历史(不落盘,pi 退出即消失)。用 /secret db 可避开。
  • DPAPI 密文绑定用户配置:管理员重置密码或重建 profile 后作废。
  • 已经写进 sessions/*.jsonl 的历史不会回溯清理。

许可

MIT