npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zxh126/pi-redact-secrets

v0.2.1

Published

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

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 add db <值> | 同上,动词形式。add / set / put 互为别名;值里带 = ? &(签名 URL)时更好写 | | /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 会原样保留,不再阻断调用(失败会推迟到命令本身)。

两端对未知 key 都是失败关闭:A 原样保留并告警(没有 UI 的渠道,比如 QQ 机器人,会把告警作为一段附注加到消息里,让模型知道这个引用被忽略了),B 直接阻断调用,而不是拿空密码去认证。

可逆的输入脱敏(injection.vaultInput)

以前对自己的消息脱敏是单向的:https://…?token=abc123&id=42 到模型这里就变成 token=<REDACTED>,想让你帮忙打开这个链接的任务直接做不了。

injection.vaultInput(默认 true)把命中的值只存在内存里(不落盘、不进 store),同时把消息改写成引用:

see https://ex.example.com/page?token=[$secret:auto1]&id=42 please

> pi-redact-secrets: 1 secret value in your message was replaced by a reference …: [$secret:auto1] -> PI_SECRET_AUTO1. …

模型拿到的是能用的引用而不是死掉的 <REDACTED>;transcript 和 session 文件里没有任何值;shell 工具能用 PI_SECRET_AUTO1。如果这个值本来就是某个 /secret key 的值,直接复用那个 key,不会再造一个 autoN;autoN 也绝不会覆盖已存储的 key。设 vaultInput: false 回到旧的单向脱敏。

后端

| 平台 | 后端 | 位置 | |---|---|---| | 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",
    "vaultInput": true,
    "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