@nicknisi/pi-cloak
v0.1.2
Published
Redact secrets and sensitive paths from model context
Maintainers
Readme
@nicknisi/pi-cloak
Redacts secrets and other sensitive content from read tool results before they enter model context. Rules are defined per-file-glob in a JSON config; when the agent reads a matching file, matched text is replaced with a masked string (e.g. S********) so credentials, tokens, and private values never reach the model while the agent can still see file structure and read around the masked regions.
What it adds
- Command:
/cloak-status— reloads the config and notifies with current status:pi-cloak enabled=<bool> patterns=<n> config=<path>, or the load error if the config is missing/invalid. - Event hooks:
session_start— reloads config; notifies a warning if the config file is missing or failed to parse (UI sessions only).tool_result— intercepts results from thereadtool only; rewrites text content parts in place when a rule matches the read path. Other tools and non-text content parts pass through untouched. Returnsundefined(no modification) when disabled, when no rule matches, or when nothing changed.
- Config file:
<agentDir>/cloak.json(see below). - Exports (for other extensions/tests):
loadState(configPath?)andcloakText(rawText, rawPath, cwd, state)are exported fromindex.ts.
No tools, keybindings, widgets, or custom entry types.
Install
pi install /Users/nicknisi/Developer/pi-extensions/packages/cloakConfiguration
Config is read from DEFAULT_CONFIG_PATH = join(getAgentDir(), "cloak.json") — i.e. ~/.pi/agent/cloak.json with a default pi setup. It is loaded at extension init and reloaded on session_start and /cloak-status. There is no live file watcher; edits take effect on the next session start or /cloak-status.
If the file is missing or invalid, the extension degrades to a disabled default config (no rules) and surfaces a warning notification on session start.
Top-level options
| Key | Type | Default | Description |
| ---------------- | ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| enabled | boolean | true | Master switch. When false, cloakText and the tool_result hook return input unchanged. |
| cloakCharacter | string | "*" | Fill character(s) used for the masked portion of a replacement. Repeated/sliced to length. |
| cloakLength | number \| null | null | Fixed length for every masked replacement. When null, length is max(match.length, visiblePrefix.length) so masks preserve original length. |
| tryAllPatterns | boolean | true | When true, every pattern in a rule is applied to each line. When false, processing stops at the first pattern in the rule that produced a change. |
| patterns | CloakRuleConfig[] | [] | List of rules (see below). |
Rule shape
{
"filePattern": "**/.env*",
"cloakPattern": "^([A-Z0-9_]+)=(.+)$",
"replace": "$1="
}| Key | Type | Required | Description |
| -------------- | --------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| filePattern | string \| string[] | yes | Glob(s) matched against the read path. Supports * (within a segment), ** (across segments, **/ also matches zero directories), and ?. Matching is attempted against the raw path, the cwd-resolved absolute path, and each path's basename, so both **/.env* and .env work. A leading @ on the read path is stripped; ~ is expanded; backslashes are normalized to /. |
| cloakPattern | string \| object \| array | yes | Regex source string(s) applied line-by-line. Object form: { "pattern": string, "replace"?: string, "flags"?: string }. The g flag is always added. |
| replace | string | no | Rule-level default replacement template; a per-pattern replace overrides it. |
Replacement semantics
Each match becomes visiblePrefix + mask:
- With a
replacetemplate, the visible prefix is the rendered template. Supported substitutions:$&(whole match),$1–$99(capture groups),$$(literal$). Unknown$xsequences are passed through literally. - Without
replace, the visible prefix is the first character of the match (e.g.hunter2→h*******). - The remainder is padded with
cloakCharacterup tocloakLength, or tomax(match.length, visible.length)whencloakLengthisnull. If the visible prefix is already longer than the target length, it is truncated and no mask is added. - Line endings are preserved (
\r\nstays\r\n). If no line changed, the original text object is returned unmodified.
Full example (~/.pi/agent/cloak.json)
{
"enabled": true,
"cloakCharacter": "*",
"cloakLength": null,
"tryAllPatterns": true,
"patterns": [
{
"filePattern": ["**/.env", "**/.env.*"],
"cloakPattern": {
"pattern": "^([A-Z0-9_]+)=(.+)$",
"replace": "$1="
}
},
{
"filePattern": "**/*.pem",
"cloakPattern": ".+"
},
{
"filePattern": "**/credentials.json",
"cloakPattern": [
{ "pattern": "\"(private_key|client_secret)\":\\s*\"([^\"]+)\"", "replace": "\"$1\": \"" },
{ "pattern": "-----BEGIN [A-Z ]+-----.*-----END [A-Z ]+-----", "flags": "s" }
],
"replace": ""
}
]
}Effect: in .env files API_KEY=sk-abc123 becomes API_KEY=*********; in .pem files every non-empty line becomes x********...; in credentials.json named fields keep their key and quote prefix with the value masked.
Usage
/cloak-statusTypical workflow: add rules to ~/.pi/agent/cloak.json, run /cloak-status in a session to reload and verify the pattern count, then any read of a matching file is masked before the model sees it.
Dependencies
@earendil-works/pi-coding-agent(peer,*) —ExtensionAPItype andgetAgentDir(); relies on the extension event API (pi.on,pi.registerCommand) and thetool_resultevent contract (event.toolName,event.input.path, mutablecontentparts).- Node built-ins only otherwise:
node:fs,node:os,node:path. No npm runtime dependencies, no workspace deps.
Caveats
- Only the
readtool is cloaked. Content reaching the model viabash(cat,grep),editdiffs, or any other tool is not masked. Rules that depend on secrecy should assume shell access can bypass the cloak. - Depends on the
tool_resultevent returning{ content }to mutate results, and onevent.input.pathbeing a string — both are pi internals that could change across versions. - Regexes are compiled with
new RegExp(spec)directly from config; an invalid regex throws duringcompileRuleinsideloadState'stry/catch, which surfaces as a "failed to load" warning and disables all rules (falls back to empty defaults). - Cloaking is line-oriented: patterns are applied per line, so multi-line secrets can only be matched if the regex itself spans within a single line (the
s/mflags do not join lines). - Glob matching is a hand-rolled
globToRegExp, not a full glob library — no character classes ([...]) or brace expansion. - The masked output length can leak the original secret length when
cloakLengthisnull(default). SetcloakLengthto a fixed number to avoid this. - Config is not hot-reloaded on file change; reload requires a new session or
/cloak-status. - Package name is
@nicknisi/pi-cloak, private, single-file extension. (Directory renamedcloak; package name kept.)
