Skip to content
Back to skills

Pii Mask

ASecurity

Замаскировать персональные данные в тексте перед облачной LLM и развернуть оригиналы в ответе - шлюз pii-mask. Брать для транскрипта, переписки, письма с ПД. Не нужен для обезличенного текста.

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
developmentbashgit

Works with

  • cli

Security analysis

A93/100
  • highPerforms destructive filesystem operations

Pro shows the line behind each finding and how to fix it

Scanned September 29, 2026

npx -y skills add dewil/claude-toolkit --skill pii-mask --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pii Mask?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pii Mask
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dewil-pii-mask/badge)](https://www.skillsdirectory.com/skills/dewil-pii-mask)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pii-mask
description: Замаскировать персональные данные в тексте перед облачной LLM и развернуть оригиналы в ответе - шлюз pii-mask. Брать для транскрипта, переписки, письма с ПД. Не нужен для обезличенного текста.
---

# Маскировка ПД перед облачной моделью

Шлюз заменяет персданные метками (`{{PERSON_1}}`) и формат-сохраняющими фейками (`+7 000 ...`, `userN@example.com`), а после ответа модели возвращает оригиналы. Соответствие "метка - оригинал" живет в mapping-файле рядом с задачей; сервис не хранит ничего.

Движок: [pii-mask](https://github.com/dewil/pii-mask) - локальный, офлайн, русскоцентричный (ПД-форматы РФ, NER и морфология русского). Не установлен - скилл неприменим, ставится оттуда.

## Главное правило: не читай оригинал

Если облачная модель - это ты сам (сессия агента), то **оригинальный файл открывать нельзя**. Прочитал `Read` - персданные уже в контексте облачной модели, и маскировать дальше нечего. Отсюда порядок:

1. Файл прогоняется через CLI **не глядя** - агент видит только путь.
2. Агент читает и обрабатывает **только** `*.masked.md`.
3. Ответ разворачивается обратно тем же CLI.

Тот же запрет касается mapping-файла: в нем лежат оригиналы списком, это самый концентрированный источник ПД во всей схеме. Не открывать, не цитировать, не пересылать.

## Порядок

### Если исходник - PDF с таблицами

**Как извлечен текст - решает больше, чем настройки шлюза.** Такой PDF превращается в текст двумя способами, и они дают разный результат:

```bash
pdftotext -layout документ.pdf текст.txt   # сохраняет колонки - и рвет сущности
pdftotext         документ.pdf текст.txt   # поток в порядке чтения - сущности целые
```

Постраничный режим сохраняет визуальную верстку, а вместе с ней разрывает то, что маскируется: ФИО разъезжается по колонкам, идентификатор рвется по дефису, ключевое слово реквизита остается на одной строке, а число уходит на другую. Замер на акте проверки (76 страниц): полных ФИО капсом видно 55 против 879, обрубков идентификаторов 978 против 11.

**Победителя на все документы тут нет - прогоните оба режима и сравните.** Поток отменяет физическую раскладку, и на документе, где раскладка и есть граница сущности (две колонки "имя - фамилия", метка реквизита слева и значение справа), он способен размотать колонки и развести то, что было парой. Критерий выбора - не читаемость текста, а целость сущностей: в потоке ФИО собрано целиком, метка стоит рядом со своим значением.

Штатная проверка после маскировки (ниже) этот промах **не поймает**: она считает остаточных кандидатов ПД, а разорванная сущность кандидатом уже не выглядит и дает честный ноль. Сравнивать надо два извлечения между собой, до маскировки.

**`pdftotext` не делает OCR.** У скана он вернет пустой текст, у смешанного документа - только страницы с текстовым слоем, молча. Прежде чем работать с результатом, сверьте объем: пусто или подозрительно мало - документ сканированный, и маскировать в нем нечего, потому что и текста нет.

### Маскировка

```bash
pii-mask mask <исходник> -o <рабочая>/имя.masked.md --mapping <рабочая>/имя.mapping.json
# при необходимости: --audit (второй проход локальной LLM), --types, --no-ner
```

Дальше агент читает `имя.masked.md`, делает свою работу (саммари, разбор, письмо), кладет результат в файл - и разворачивает:

```bash
pii-mask unmask <результат> --mapping <рабочая>/имя.mapping.json -o <результат>.final.md
```

**Проверить, что маскировать сейчас можно,** - двумя командами, и обе нужны:

```bash
pii-mask where                                           # откуда работает код: путь импортированного модуля
D=$(mktemp -d)   # приватная папка: mapping пробы не должен лежать по угадываемому пути
echo "Иван Петров, +7 912 345-67-89" | pii-mask mask - --mapping "$D/probe.json"; RC=$?
rm -rf "$D"; [ $RC -eq 0 ] || echo "!!! проба маскирования упала (код $RC)"
```

Первая показывает, какая копия исполняется (форма 4 в `silent-failure.md`): путь должен вести в установленный экземпляр; рабочее дерево репозитория или снесенный с диска каталог - поломка. Вторая - что маскирование работает на заведомо непустом входе: имя в выводе заменено меткой (`{{PERSON_1}}`), телефон - форматным фейком (`+7 000 ...`). Проба упала или имя с телефоном остались как были - маскировать нельзя, текст модели не отдается.

**Живость сервиса проверкой не считается.** `systemctl ... is-active`, открытый порт и `/health/live` с ответом `ok` молчат одинаково при исправном и мертвом маскировщике - прецедент 28.09.2026: HTTP-шлюз месяц работал из удаленного каталога, Linux держал открытый файл, и все три признака были зелеными, а каждый запрос `/mask` возвращал 500. Health-ручка остается для мониторинга самого сервиса.

**Основной путь - CLI.** HTTP-путь (`POST /mask`, `POST /unmask`) - вариант для того, кто поднял сервис осознанно и сам следит, из какой копии он работает: второй канал живет своей жизнью и протухает незаметно, а выигрыш в скорости - секунды (замер: CLI 2,7 с против 0,8 с у сервиса). Сервис запускается из установленного экземпляра. Из рабочего дерева репозитория нельзя: там незакоммиченная правка, переключенная ветка или проверка теста меняют поведение боевого маскировщика.

## Куда класть рабочие файлы

`--mapping` и `-o` задавать **явно**, а не полагаться на дефолт: по умолчанию оба файла ложатся рядом с исходником, а исходник обычно лежит в синкаемой папке или в git-репозитории. Mapping с настоящими ПД, уехавший в облачный синк или в коммит, - ровно та утечка, ради предотвращения которой все и затевалось.

Рабочая папка - локальный временный каталог сессии. Права `600` на mapping шлюз ставит сам.

## Когда включать `--audit`

Второй проход локальной модели ловит косвенное - "моя жена Оля", адрес без формата, ник без собачки. Regex и NER такого не видят.

Цена - время: порядка получаса машинного счета на транскрипт часовой встречи (CPU). Это батч-режим: запускать в фоне и не ждать в диалоге. Для короткого текста, где важна только форматная часть (телефоны, почты, ИНН), аудит не нужен.

Аудит fail-closed: запрошен, а локальная LLM недоступна - будет ошибка, а не тихая маскировка без аудита.

## Что проверить после маскировки

Прежде чем отдавать текст модели - убедиться, что маскировка сработала, **не вынося значения наружу**: посчитать остаточных кандидатов ПД в замаскированном тексте и дословные утечки оригиналов из mapping, напечатав только счетчики и типы.

Ноль - можно работать. Не ноль - **смотреть глазами локально**, а не печатать найденное в лог сессии. Счетчик говорит "что-то не так", но не "что именно": на живом транскрипте расхождение счетчиков означало не пропущенные ПД, а ложную сущность - NER принял за имя обычное слово, и в тексте остались другие его вхождения. Диагноз по одним счетчикам ставить нельзя, и тем более нельзя по ним править код.

## Чего скилл не делает

Скилл закрывает один сценарий: **текст уходит машине**, и кривая маскировка там не страшна - лишняя маска мешает модели чуть-чуть, а пропуск ловится тем, что модель персданных не запоминает.

Второй сценарий - **обезличить документ, который прочитает человек** (кейс для показа, публичный разбор, портфолио) - этим скиллом не решается. Машинный проход годится там только как **детектор**: показать, что вы забыли. Исполнителем он быть не может по двум причинам: оставляет квазиидентификаторы (см. ниже) и портит текст ложными масками, а документ для человека обязан читаться. Такой документ **переписывается вручную**, а не маскируется.

### Квазиидентификаторы: чего движок не поймает по определению

Он ищет прямые идентификаторы - имя, телефон, почту, домен. Человека же опознают их сочетания, каждое из которых по отдельности безобидно:

- работодатель плюс город плюс точные даты работы;
- уникальный или редкий тайтл должности;
- вуз плюс год выпуска плюс специальность;
- узкая предметная область на маленьком рынке.

Реальный случай: "руководитель направления аналитики сервисных функций и взыскания" в конкретном банке - это один человек на рынке, и вычищенное имя не спасает.

Лечится не маской, а **огрублением**: "банк из топ-20" вместо названия, "2019-2024" вместо точных месяцев, "руководитель аналитического направления" вместо точного тайтла. Огрубление - решение автора документа, не агента: степень допустимой потери смысла знает только он.

## Границы

- Имена возвращаются в именительном падеже: "заявка для Иван Петров". Это осознанное ограничение шлюза, не баг.
- Города и страны по умолчанию не маскируются - в рабочих текстах это чаще контекст, чем ПД.
- NER обучен на новостях: спонтанная речь и редкие имена даются хуже, а расшифровка без нормальной пунктуации дает ложные срабатывания на обычных словах. Для чувствительного текста - `--audit`; для расшифровок - проверять результат.
- Разнос сущности по документу ("нашли один раз - замаскировать все вхождения") допустим только от **детерминированного** признака: организационно-правовая форма плюс название в кавычках. От находок NER так делать нельзя - одно ложное срабатывание размножится на весь документ и сделает текст непригодным для модели, ради которой все и делается. Разносятся различающие слова названия, а не родовые: у "ООО Бюро кредитных историй Ромашка" по документу поедет "Ромашка", но не "бюро" и не "кредитных". Короткая аббревиатура - только в своем регистре: "ОКБ" это организация, "окб" внутри слова случайность.
- Цена разноса - лишняя маска, и она никуда не делась: различающее слово может совпасть с обычным (название "Мир" замаскирует слово "Мир" в соседней фразе) или с фамилией человека (название "Иванов" и директор Иванов получат одну ORG-метку). Список родовых слов в движке конечный, за его пределами это не ловится. Название короткое или словарное - проверить, что разнос не задел лишнего, прежде чем отдавать текст модели.
- Mapping удаляется вместе с задачей. Это единственное место, где ПД существуют вне исходника.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.