@k3_2o/pi-secrets0.2.1
A pi extension that reversibly obfuscates credentials before they reach the provider: keyed $$PLACEHOLDER$$ substitution, built-in credential patterns, and a user secrets.yml for custom redaction.
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.
Reports 0
No reports yet.
Readme
pi-secrets
A pi extension that keeps real credentials out of the model's context and out of your session files and exports. API keys and tokens are replaced with reversible
$$PLACEHOLDER$$hashes before every provider call; user and assistant text is stored hashed at rest (a paint-time reveal shows you the real values); tool output is scrubbed before it is persisted; and HTML/JSONL exports contain only hashes. "Store safe, show real".
Features
- The model never receives a real secret.
contextobfuscates every message before every LLM call: user text, tool results, and prior assistant replay (text, thinking, tool-call arguments). - Files and exports are clean.
message_endhashes user and assistant text at rest;tool_resultscrubs tool output (content andstructuredContent);user_bashscrubs!-command output. HTML and JSONL exports derive from the file, so they are clean by construction. - You still read real values. Your own messages render with real values at paint time.
Tool-output blocks show
$$…$$: pi draws them with the same renderer used for HTML exports, so a reveal there would leak into the exported file. - Tools still work on real bytes. Edits and commands run with real values; the model
plans against placeholders (
edit old="$$HASH$$"matches the file). - Learn-on-first-sight. A
KEY=valueline or URL password that is not registered yet is captured the first time it appears and hashed like any registered secret. Nosecrets.ymlentry needed. - One-way bullets match capture. Credential-named assignment values under the 8-char capture floor stay raw entirely; bullets and capture share one length gate.
- Hash morphing done right.
$$NAME_HASH:HINT$$placeholders are HMAC-keyed per install: deterministic (provider prompt-cache stable), unforgeable, and undictionaryable without your key file. - Custom redaction. Add
secrets.ymlentries for anything the built-ins miss: exact strings or regexes, reversible hashes or one-way replacements (table below). - Off by default. No behavior change until
secrets.enabled(or the--secretsflag).
Quick start
Install the extension as a pi package:
pi install npm:@k3_2o/pi-secrets # published on npm pi install git:github.com/k3-2o/pi-secrets # straight from the repo pi install ./pi-secrets # local checkoutTry it without installing:
pi -e ./pi-secrets. Manage installs withpi list,pi remove <source>, andpi update --extensions.Enable it in
~/.pi/agent/settings.json:{ "secrets": { "enabled": true } }or per session with the flag:
pi --secrets.Smoke test with a fake key:
TEST_SMOKE_API_KEY=sk-smoke-1234567890abcdef piAsk the agent to
readthe value ofTEST_SMOKE_API_KEY. It sees$$SMOKEKEY_…$$, never the real string. Quote the placeholder back and your terminal shows the real key; the session file stores the placeholder.
What is stored where
| Message class | Session file (.jsonl) |
Your terminal | HTML / JSONL exports |
|---|---|---|---|
| User text (typed/pasted) | $$HASH$$ |
real (paint reveal) | $$HASH$$ |
| Assistant text | $$HASH$$ |
real (paint reveal) | $$HASH$$ |
| Tool call arguments | $$HASH$$ (the model's own args) |
(none) | $$HASH$$ |
| Tool output | $$HASH$$ / •••••• (scrubbed) |
$$HASH$$ |
$$HASH$$ |
!-command output |
$$HASH$$ (scrubbed) |
real until recorded, then hashed | $$HASH$$ |
Tool-output blocks show $$…$$ even on your screen: pi draws them with the same
renderer used for HTML exports, so a reveal there would leak into the exported file.
Everything else you type and read renders with real values.
Editing files that contain secrets
Two kinds of protection, two edit realities:
Registered values ($$HASH$$) are editable, safely. The model only ever sees the
placeholder, but pi-secrets restores the real value into tool-call arguments at
execution time. So edit calls that reference a line containing a registered value
keep working (exact old-string matching succeeds after restoration) while the
provider and every persisted artifact only ever saw the hash. This is why built-in
credential shapes are hash-routed: edits around known secrets stay lossless.
Unregistered values are captured or raw, never silently reusable. When tool output
contains a credential-named assignment (API_KEY=…) or a connection-URL password,
pi-secrets auto-captures the value on first sight. It registers the value
session-scoped and hashes it, so it becomes editable exactly like a registered value
(two-way round-trip). A bare value in prose that matches no name gate and no
built-in shape stays raw. That is the honest boundary. Values under the 8-char floor
stay raw everywhere (capture and the bullet net share one length gate); one-way
bullets never apply to assignments.
Ways the agent can still touch such a file, honestly:
- Register the value with a
secrets.ymlentry, or export it as a credential-named env var: the••••••becomes$$HASH$$and normal edits round-trip again. - Whole-line rewrites from the shell, which never need the old value:
sed -i 's/^API_KEY=.*/API_KEY=newvalue/' file. - Side note: a hashline-based edit tool (replaces by line number from a recent read) can rewrite a line without knowing its old content. Still, for a bulleted value the agent has nothing real to write, so it only helps registered values or human-supplied replacements.
Usage
Built-in patterns
Credential shapes are matched automatically, in four groups:
- Git & CI: GitHub (
ghp_,gho_,github_pat_), GitLab (glpat-), npm - Cloud & AI: AWS (
AKIA/ASIA), Google (AIza), OpenAI (sk-,sk-proj-,sk-ant-), HuggingFace (hf_) - Payments & comms: Stripe (
sk_/rk_live/rk_test,whsec_), SendGrid (SG.), Twilio (SK+32 hex) - More: Slack (
xox*), JWTs (eyJ…),Bearer …tokens, PEM private-key blocks, Shopify (shpat_/shpca_/shppa_), Postman (PMAK-), Pulumi (pul-), DigitalOcean (dop_v1_), Grafana Cloud (glc_), Alibaba (LTAI)
Environment variables whose names match KEY, SECRET, TOKEN, PASSWORD, AUTH,
CREDENTIAL, PRIVATE, or OAUTH are also added at startup (values of 8+ characters,
plus passwords embedded in scheme://user:password@host URLs).
Where secrets are found
At session start, pi-secrets registers values from three sources and obfuscates any occurrence of them that would otherwise reach the model or the disk:
- Environment variables with credential-shaped names (
KEY,SECRET,TOKEN,PASSWORD,AUTH, …) secrets.yml(global + project)- Built-in vendor shapes (GitHub, OpenAI, AWS, Google, Slack, Stripe, JWT, PEM, …)
The boundary: only recognized values are hidden. A bare value in prose that
matches no source and no shape passes through as-is. But a credential-named
assignment (API_KEY=…) or connection-URL password is auto-captured on first
sight and hashed ($$HASH$$, reversible) with no secrets.yml line needed. To cover
anything else, add a secrets.yml entry or export it as a credential-named
environment variable.
The three nets
| Net | Hardcoded trigger | What it catches |
|---|---|---|
| Auto-capture (learn-on-first-sight) | Credential-named assignments (name contains KEY, SECRET, TOKEN, PASSWORD, PASSWD, PASS, AUTH, CREDENTIAL, PRIVATE, or OAUTH) and scheme://user:password@host URLs, values of 8+ chars |
Assignment/URL values that were never registered and hashed on first sight, session-scoped, reversible |
| Built-in shapes | Vendor prefixes (sk-, ghp_, glpat-, AKIA, eyJ, …) |
Bare prefixed tokens, anywhere, no assignment needed |
secrets.yml |
Your entries | Everything the two automatic nets miss |
What stays raw: anything that looks like neither a KEY=value line nor a recognized
shape. A bare vendor token in prose, a key under a non-keyword name (NXOP=…), or a
JSON field whose name carries no keyword is only hidden if you register it in
secrets.yml. URL passwords are the one exception: their shape alone triggers capture.
Dump formats are read through (od -c, hexdump/xxd, base64); the full boundary is in docs/explain.md.
Custom patterns: secrets.yml
Global: ~/.pi/agent/secrets.yml. Project: <cwd>/secrets.yml (project entries
override globals with the same content).
See examples/secrets.yml for a full, annotated example covering every field and mode.
| Field | Values | Meaning |
|---|---|---|
type |
plain | regex |
Exact string vs. pattern match |
content |
string | The literal or pattern to hide |
mode |
obfuscate (default) | replace |
Reversible placeholder vs. one-way replacement |
replacement |
string | Replace-mode output. Omit to get a deterministic same-length replacement (stable across sessions) |
flags |
regex flags, e.g. "i" |
Only for regex entries |
friendlyName |
string | Human-readable placeholder prefix, e.g. $$CLI_TOKEN_…$$ |
Configuration
| Key | Type | Default | Meaning |
|---|---|---|---|
secrets.enabled |
boolean | false |
Master switch |
--secrets flag |
boolean | unset | Force the feature on/off for a session |
The per-install key lives at ~/.pi/agent/secret-placeholder.key (mode 0600,
created lazily on the first placeholder mint). Delete it to invalidate old
placeholders. See Rotation in docs/explain.md.
Documentation
- docs/explain.md: important read. The threat model, protection matrix, what is not protected, and rotation.
Contributing
Run just check (format, lint, types, tests) and keep it green.
License
MIT. See LICENSE.
Inspired by OMP's secret obfuscation design.