Security·terminal·MIT
pii-guard
Reversible Taiwanese PII redaction for tool results, prompts and writes, all on loopback.

pii-guard replaces Taiwanese personal data with placeholders before Claude Code’s model reads it, and puts the real values back when Claude writes a file or runs a shell command. Detection, the mapping table and every allow-or-deny decision live in pii-guard-hookd, a Python service on your machine. The mod does no detection of its own: it moves values between Claude Code’s events and that service over loopback HTTP.
It is the Claude Mods front end of pii-guard, a larger project for reversible de-identification of Traditional Chinese (Taiwan) documents. The 142 GitHub stars belong to that project, not to the mod alone; its READMEs are in Traditional Chinese. Compared with secret-redactor, which does the same round trip for API secrets, the table here is held by a separate service.
pii-guard already ships classic hooks that do the same job. The mod exists for one gap: classic PreToolUse runs after Edit has compared old_string with the file, so a model that saw <TW_MOBILE_1> and puts it in an Edit always fails with “String to replace not found”. The mod’s tool.call runs before that comparison, restores the placeholder, and the Edit succeeds. The other visible change is that a prompt carrying personal data is rewritten instead of blocked.
What it does
Placeholders look like <PERSON_1>, <TW_MOBILE_1>, <TW_NATIONAL_ID_1>, <ORG_1>, <LOCATION_1> and <EMAIL_ADDRESS_1>. The engine detects names, organisations and places with CKIP’s Chinese NER model; Taiwan national ID, ARC, passport and business ID numbers, mobile and landline numbers, licence plates, birth dates and bank accounts with regex; and email addresses and card numbers with Presidio’s built-in recognisers. Nothing in the README claims coverage of English names: this is built for Taiwanese data. A /pii-terms <spreadsheet> command registers a customer list as terms to always mask, reading only its column names.
Install
Needs Claude Code 2.1.287 or later, Python 3.11 or later (3.13 is the target), uv and a python3 on your PATH. The first run downloads the CKIP NER model, about 500 MB. The author names no tested Claude Code version: the checks were run in interactive and -p mode with Sonnet, verified against the transcript.
git clone https://github.com/danyuchn/pii-guard.git
cd pii-guard
uv sync
uv run pii-guard-hookd install --mod
claude --plugin-dir ~/.claude/plugins/pii-guard
install --mod links the plugin to a fixed path under Claude Code’s config directory, removes the classic hook entries the mod replaces, and prints the launch command. uv run pii-guard-hookd doctor --mod checks the install and runs claude plugin validate; uv run pii-guard-hookd uninstall removes it. The service starts on demand through a classic SessionStart hook that stays installed, or by hand with uv run pii-guard-hookd serve.
There is no marketplace: --plugin-dir is the only way to load the mod, and a session started without the flag has no protection at all.
How it works
The module reads state.env from the service’s home with $.fs.read to get the port and a bearer token. The home is options.hookdHome, then PII_GUARD_HOOKD_HOME, then ~/.local/share/pii-guard/hookd/ by default. Each event is POSTed as JSON to http://127.0.0.1:<port>/v1/hooks/<event> through $.http.fetch, with the session id attached. If that call is refused, as it is under claude plugin eval, it falls back to $.process.run with a stdlib python3 one-liner that POSTs to the same loopback address. No request leaves the machine, and no detection model runs inside Claude Code.
Per event:
session.startasks the service for a briefing and loads the seed terms. The briefing rides as context on the first prompt, since asession.startresult cannot put text in front of the model.prompt.submitsends your text and the working directory and submits what comes back: personal data replaced by placeholders, which the service remembers for later restoration, and each@filereference turned into an instruction toReadthat path, so the content enters through the guarded tool path.tool.callsends the input ofWrite,Edit,MultiEdit,Bash,WebFetch,WebSearch,Agent,Task,Workflowandmcp__*and applies the answer: adenystring, or a rewritten input with placeholders restored. Per the README the service refusesWebFetch,WebSearch, MCP tools and remote agents, plus shell commands that carry a placeholder and can reach the network (curlin the author’s test). After the tool runs, the result ofRead,Bash,Grepandmcp__*goes to the service and comes back de-identified, with optional context lines for the model.WriteandEditresults are left alone: the model only reads “File created successfully”.session.compactsends the messages before compaction and the summary after it, both swept for values the session already knows. No detection runs on the transcript.
The mapping table stays in the service, one per session, inside its home directory (owner-only on POSIX, a tightened ACL on Windows).
The module fails closed. With the service unreachable, a guarded tool call is denied, a Read or Bash result is withheld with a note telling the model to have you start the service, a prompt is dropped, and a compaction is skipped. Every registration carries a .catch, because a hook that throws or exceeds the 10 s budget is skipped and the tool beneath it would run unguarded.
Two classic hooks remain after install --mod. MessageDisplay restores placeholders on your screen, so you see real values while the model sees placeholders; function hooks have no equivalent event. SessionStart --start-only starts the service, because a hooks module cannot spawn a process that outlives the session.
Token cost is small but not zero: the briefing on the first prompt, context lines on redacted results, and offline notices. The author does not quantify it.
Limitations
- The raw prompt you type still lands in the local transcript. Claude Code records user input in a
queue-operationentry under~/.claude/projects/before the rewrite. The model does not see it, but the value is on disk. - File paths are not de-identified, because a rewritten path has to remain readable. A name in a path reaches the model.
- Detection is probabilistic. The main README warns that its synthetic test corpus overstates real-document performance.
More mods
The whole directory →anchorwatch-mod
Blocks rm -rf, force pushes, DROP TABLE, curl | sh and cat .env before they run.
Assertion Guardian
Refuses test edits that drop assertions, add skips or change expected values.
blast-radius
Shows what rm -rf or a force push would change and waits for Proceed or Cancel.
collision-guard
Asks before Claude edits a file another open chat just changed.


