Claude Modsunofficial directory

Security·terminal·MIT

pii-guard

Reversible Taiwanese PII redaction for tool results, prompts and writes, all on loopback.

pii-guard has no screenshot yet: generic placeholder cover, a terminal with the band above the prompt

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.start asks the service for a briefing and loads the seed terms. The briefing rides as context on the first prompt, since a session.start result cannot put text in front of the model.
  • prompt.submit sends 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 @file reference turned into an instruction to Read that path, so the content enters through the guarded tool path.
  • tool.call sends the input of Write, Edit, MultiEdit, Bash, WebFetch, WebSearch, Agent, Task, Workflow and mcp__* and applies the answer: a deny string, or a rewritten input with placeholders restored. Per the README the service refuses WebFetch, WebSearch, MCP tools and remote agents, plus shell commands that carry a placeholder and can reach the network (curl in the author’s test). After the tool runs, the result of Read, Bash, Grep and mcp__* goes to the service and comes back de-identified, with optional context lines for the model. Write and Edit results are left alone: the model only reads “File created successfully”.
  • session.compact sends 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-operation entry 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.