Content
# docs-masked
Локальное обезличивание документов перед отправкой в языковую модель — и
обратная подстановка после ответа.
Документ никогда не покидает машину в исходном виде. Персональные данные
заменяются устойчивыми тегами (`#PERSON_1#`, `#PHONE_2#`, `#ADDRESS_1#`), в
модель уходит только текст с тегами, а полученный ответ восстанавливается
локально по сейфу соответствий.
```
документ ──▶ маска ──▶ контроль утечки ──▶ модель ──▶ обратная подстановка
локально локально сеть локально
```
Работает как скилл для Claude Code, как MCP-сервер для любого другого агента
и как обычная утилита командной строки.
## Как это работает
**1. Маска.** Документ разбирается на текстовые фрагменты — абзацы, ячейки,
узлы разметки. В каждом находятся персональные данные, каждое значение
получает устойчивый тег. Один и тот же человек получает один и тот же тег по
всему документу, включая падежные варианты и инициалы: «Иванов Иван Иванович»,
«Иванову» и «Иванов И.И.» — это один `#PERSON_1#`.
**2. Контроль утечки.** Замаскированный текст повторно прогоняется через все
детекторы плюс параноидальный проход: любой `@`, любая цепочка из семи и более
цифр, любой телефоноподобный набор. Если что-то осталось — отправка
**блокируется** исключением, а не предупреждением в логе.
**3. Отправка.** Наружу уходит только текст с тегами. Единственная точка
выхода в сеть — функция `llm.send()`, и она обязана вызвать проверку до
запроса. Каждая отправка пишется в журнал `~/.pii_shield/egress.jsonl`: время,
провайдер, модель, размер, sha256, статус проверки. Содержимое не пишется.
**4. Обратная подстановка.** Ответ модели проходит через сейф: теги заменяются
на оригиналы. Для ФИО подставляется восстановленный именительный падеж — если
в документе человек упомянут только как «Кузнецову Ивану Петровичу», в ответе
он станет «Кузнецов Иван Петрович».
## Установка
```bash
git clone https://github.com/kpshinnik/docs_masked.git ~/.docs_masked/src
cd ~/.docs_masked/src && ./install.sh
```
Скрипт поставит зависимости, положит скилл в `~/.claude/skills/docs-masked` и
напечатает готовый фрагмент конфигурации MCP. Подробности и варианты —
в [docs/INSTALL.md](docs/INSTALL.md).
## Подключение к агенту
| Способ | Кому | Как |
|---|---|---|
| **Скилл** | Claude Code, Claude.ai | `./install.sh` либо `/plugin marketplace add kpshinnik/docs_masked` |
| **MCP-сервер** | Cursor, Windsurf, Codex CLI, Continue, Zed, Cline, Claude Desktop | `python3 mcp_server.py` как stdio-сервер |
| **CLI и правило** | всё остальное | команды в терминале плюс `templates/AGENTS-rule.md` в свой проект |
Пошагово по каждому харнесу — [docs/HARNESSES.md](docs/HARNESSES.md).
MCP-сервер написан без зависимостей: нужен только `python3`. Он отдаёт шесть
инструментов — `mask_text`, `unmask_text`, `verify_text`, `scan_document`,
`mask_document`, `unmask_document`.
## Использование
```bash
docs-masked scan договор.docx # что будет скрыто
docs-masked mask договор.docx # маска + сейф
docs-masked report договор.docx --open # посмотреть глазами
docs-masked ask договор.docx -p "Найди риски по срокам"
docs-masked unmask договор.masked.docx --vault договор.docx.vault.json
```
### Команды
| Команда | Что делает |
|---|---|
| `scan FILE` | Показывает, что будет замаскировано. Файл не меняется, сеть не трогается. |
| `mask FILE` | Обезличенная копия в том же формате плюс файл сейфа. |
| `unmask FILE --vault V` | Возвращает оригиналы. |
| `verify FILE` | Проверяет, что персональных данных не осталось. |
| `ask FILE -p "..."` | Полный круг: маска → проверка → модель → восстановленный ответ. |
| `report FILE` | HTML-страница ревью: каждая замена в контексте, значения закрашены. |
| `selftest` | Самопроверка круговорота. |
Полный список флагов — [skills/docs-masked/references/cli.md](skills/docs-masked/references/cli.md).
## Что распознаётся
ФИО в любом падеже (русские, латиница, транслит), организации, адреса, почта,
телефоны, паспорт и код подразделения, СНИЛС, ИНН, ОГРН, КПП, БИК, расчётные
счета, банковские карты, IBAN, полисы ОМС, водительские удостоверения,
автомобильные номера, IP-адреса, `@никнеймы`, даты рождения и выдачи
документов, коды реквизитов (ОКТМО, ОКПО, КБК), плюс ваши собственные строки.
Идентификаторы проверяются по-настоящему: контрольная сумма СНИЛС, контрольные
разряды ИНН и ОГРН, алгоритм Луна для карт, mod-97 для IBAN. Полная таблица —
[references/coverage.md](skills/docs-masked/references/coverage.md).
## Форматы
| Формат | Чтение | Запись на место |
|---|---|---|
| `.txt` `.md` `.rst` `.log` `.tex` `.yaml` `.ini` | да | да |
| `.docx` | да | да, с сохранением форматирования |
| `.xlsx` `.xlsm` | да | да |
| `.csv` `.tsv` | да | да |
| `.json` | да | да |
| `.html` `.htm` | да | да |
| `.pdf` | да | по флагу `--pdf-redact`, с физическим вымарыванием |
| `.rtf` `.doc` `.odt` | да | нет (только macOS, через `textutil`) |
DOCX обходится по XML, а не через `document.paragraphs`: иначе теряются
абзацы внутри полей контента и надписей — на настоящем договоре из-за этого
пропадала целая колонка блока реквизитов. В таблицах заголовок колонки
используется как контекст: ячейка `500100732259` сама по себе неотличима от
случайного числа, а в колонке «ИНН» распознаётся уверенно.
## Python API
```python
from pii_shield import ask_document
res = ask_document("договор.docx", "Составь резюме и найди риски",
provider="anthropic")
print(res.answer) # имена уже восстановлены
```
Ручной контроль каждого шага:
```python
from pii_shield import mask_text, assert_clean, unmask_text
r = mask_text(raw) # r.text — с тегами, r.vault — сейф
assert_clean(r.text) # LeakGuardError, если что-то осталось
answer = call_model(r.text) # наружу уходит только маска
final, unknown = unmask_text(answer, r.vault, mode="canonical")
```
Подробнее — [references/api.md](skills/docs-masked/references/api.md).
## Сейф соответствий
Сейф — единственное, что связывает теги с оригиналами. Без него обратная
подстановка невозможна.
- Пишется рядом с документом как `<файл>.vault.json`, права `0600`.
- Шифруется по флагу `--pass-env` (scrypt + Fernet).
- Хранит каноничную форму, все встреченные варианты и журнал вхождений в
порядке документа — благодаря журналу точное восстановление возвращает
исходную словоформу, а не каноничную.
- Внесён в `.gitignore`. Не коммитьте его.
## Точность и границы
Инструмент устроен так, чтобы **ошибаться в безопасную сторону**: лучше
замаскировать лишнее, чем пропустить. Что стоит знать:
- **Скан-PDF без текстового слоя** не обрабатывается — нужен OCR.
- **Однофамильцы без инициалов** получают отдельные теги, а не сливаются в
одного человека.
- **Голое число без подсказок** может быть не распознано как идентификатор —
но параноидальный проход всё равно не выпустит такой текст наружу.
- **Произвольные латинские имена** без славянских окончаний и без обращения
(`Mr.`, `Dr.`) не распознаются: ловить любую пару заглавных слов дало бы
больше вреда, чем пользы.
На критичном документе стоит один раз посмотреть `docs-masked report` глазами.
## Разработка
```bash
python3 -m pytest tests/ -q # тесты
python3 -m pii_shield.cli selftest
python3 samples/make_samples.py # пересоздать тестовые документы
```
Инварианты, которые нельзя ломать, перечислены в [AGENTS.md](AGENTS.md).
Всё в `samples/` синтетическое; каталог `examples/` зарезервирован под ваши
локальные документы и в репозиторий не попадает.
## Лицензия
MIT.
Connection Info
You Might Also Like
No recommendations available