# ctxguard — санитизация контекста AI-агента > **Исторический документ.** Это проектный замысел на 2026-08-26, сохранённый как > есть: он объясняет, *почему* система устроена так, а не описывает текущее > состояние. Часть запланированного здесь не была реализована в том виде > (`references/workflows.md`, цель `make install`, `tests/corpus/`, плагин-«ведро» > для простых скиллов). Актуальное состояние — в README.md. ## Context Работая через Claude Code, в контекст модели утекает всё, что агент прочитал: содержимое файлов, вывод команд, вставленные тикеты и письма. Это значит, что провайдеру уходят креды, названия компаний-клиентов, ФИО, внутренние хостнеймы и данные продакшена. Цель — сделать так, чтобы этого не происходило, и чтобы гарантия не зависела от того, помнит ли агент правила. **Главное архитектурное ограничение.** Скилл сам по себе защитить не может: скилл — это инструкции для модели, а модель читает их уже внутри контекста, который уже отправлен. Перехват обязан быть вне модели. Единственная такая точка в Claude Code — хуки. Проверенные возможности хуков (docs.claude.com/hooks, проверено 2026-08-26): | Хук | Может | Не может | |---|---|---| | `PreToolUse` | `permissionDecision: allow/deny/ask`, **`updatedInput`** (shallow merge, при нескольких хуках выигрывает последний) | — | | `PostToolUse` | `additionalContext`, exit 2 = фидбек | **переписать/вырезать вывод — поля `updatedOutput` нет** | | `UserPromptSubmit` | `additionalContext`, exit 2 = блок | **переписать текст промпта** | | `SessionStart` | `additionalContext`, матчеры `startup/resume/clear/compact/fork` | блокировать | | `PreCompact` | запретить компактинг | повлиять на то, что сохранится | | egress / транскрипт | — | **перехвата на пути к API нет** | Следствие: **вся санитизация делается на `PreToolUse` через `updatedInput` — до запуска инструмента.** Вывод чиним, переписывая вход, который его порождает. Решения, зафиксированные с заказчиком: - Защищаем все четыре класса: секреты, бизнес-идентификаторы, PII, данные клиентов. - Обратимые псевдонимы для имён/компаний/PII; **секреты гасим безвозвратно** (метка ``). - Периметр: ввод/вывод инструментов **и** содержимое репозитория. - Упаковка: плагин Claude Code (скилл + хуки + скрипты). - **Fail-closed везде**: скрипт упал, таймаут, непонятный ввод → тул-колл блокируется. --- ## Организация репозитория Репозиторий `/path/to/skills` пуст. Делаем его **marketplace**, чтобы будущие скиллы ложились туда же по внятному правилу. ``` skills/ ├── .claude-plugin/marketplace.json # манифест маркетплейса, name: ctx-tools ├── README.md # что тут, как поставить, как добавить скилл ├── .gitignore # словари/кеш/аудит НИКОГДА не в git ├── Makefile # test | verify | install | lint ├── docs/specs/2026-08-26-ctxguard-design.md └── plugins/ ├── ctxguard/ # нужны хуки → свой плагин │ ├── .claude-plugin/plugin.json │ ├── hooks/hooks.json │ ├── skills/context-sanitization/ │ │ ├── SKILL.md │ │ └── references/{policy.md,threat-model.md,workflows.md,troubleshooting.md} │ ├── commands/{ctx-entity.md,ctx-sanitize.md,ctx-verify.md,ctx-audit.md} │ ├── scripts/ctxguard/ # Python 3 stdlib-only │ └── tests/{unit/,corpus/,run.sh} └── -skills/ # «ведро» для простых скиллов без хуков ├── .claude-plugin/plugin.json └── skills//SKILL.md ``` **Правило размещения:** скиллу нужны хуки, команды или MCP → отдельный плагин в `plugins/`. Скилл — это чистые инструкции → каталог внутри `plugins//skills/`. Одна установка покрывает все простые скиллы, хук-плагины ставятся осознанно и по одному. Установка: `/plugin marketplace add /path/to/skills`, затем `/plugin install ctxguard@ctx-tools`. Конвенции берём из установленных плагинов (это де-факто стандарт здесь): - Фронтматтер `SKILL.md`: только `name` + `description` в третьем лице со «Use when…» (образец: `~/.claude/plugins/cache/claude-plugins-official/superpowers/6.3.0/skills/writing-skills/SKILL.md`). - Progressive disclosure: детали в `references/*.md`, не в `SKILL.md`. - Скрипты — Python 3 **только stdlib**, вызов `python3 "${CLAUDE_PLUGIN_ROOT}/..."` (образцы: `~/.claude/skills/jira/scripts/jira.py`, плагин `hookify`). - Шаблон `hooks.json` — `~/.claude/plugins/marketplaces/claude-plugins-official/plugins/hookify/hooks/hooks.json`. - Принцип «нашёл, но не цитирую» уже реализован в `~/.claude/plugins/marketplaces/claude-plugins-official/plugins/claude-security/scripts/lib/secret.py` (функция `withheld()`) — переиспользуем идею как инвариант CLI. --- ## Хранилище Всё **вне репозитория**, чтобы не попало в git и не попалось агенту под руку. JSON, а не YAML — в stdlib нет парсера YAML, а `tomllib` умеет только читать. ``` ~/.claude/ctx-guard/ ├── policy.json # режим, префикс алиасов, правила, deny-списки ├── projects//entities.json # словарь real↔alias, 0600 ├── projects//index.json # кеш «в каком файле есть сущности» (по mtime+size) ├── cache//… # санитизированные двойники файлов, 0700 └── audit.jsonl # 0600, только alias/rule_id, без plaintext ``` **Два жёстких инварианта:** 1. Весь `~/.claude/ctx-guard/` в deny-read для агента, и `Bash` с упоминанием этого пути тоже. Иначе агент прочитает словарь и одним чтением сольёт всё сразу. 2. **В CLI нет ни одной ветки кода, печатающей реальное значение сущности.** Только алиасы, типы и хеши. Это делает утечку через сам инструмент невозможной, а не маловероятной. --- ## Матрица перехвата Всё через `PreToolUse`, один хук-владелец на инструмент (иначе гонка `updatedInput`). | Поверхность | Механизм | Enforced | |---|---|---| | Промпт пользователя | `UserPromptSubmit`: детект → exit 2 + причина с подсказкой `/ctx-sanitize` | да, блоком | | Строка команды Bash | `updatedInput.command` — обратная подстановка alias→real | да | | **Вывод Bash** | `updatedInput.command` → `python3 ctxguard run -- `: обёртка сама запускает команду и стримит отфильтрованный вывод | да | | Содержимое `Read` | `updatedInput.file_path` → путь санитизированного двойника в кеше | да | | `Grep`/`Glob` | `path` → каталог двойников; `pattern`, совпавший с сущностью, — deny | да | | `Write`/`Edit` | `content` / `old_string` / `new_string` — обратная подстановка alias→real и twin-path→real-path | да | | `WebFetch`/`WebSearch` | deny при сущности или секрете в url/query | да | | MCP (`mcp__.*`) | скан всех строковых аргументов, семантика неизвестна → deny при попадании | да | | `Task` (сабагенты) | скан аргумента `prompt` | да | | Скриншоты, изображения | **не покрыто** | нет | | Текст, сгенерированный самой моделью | **не покрыто** | нет | Ключевой приём — обёртка Bash. Она же бесплатно закрывает `git log`, `git blame`, `git remote -v` и почтовые адреса коммитов, которые иначе текут мимо любой фильтрации файлов. **Симметрия путей обязательна.** `Read` отдаёт двойник → агент дальше правит *двойник*. Поэтому `Write`/`Edit` обязаны отображать twin-path обратно в реальный путь и делать обратную подстановку содержимого. Модуль `pathmap` двунаправленный; без него правки уходят в кеш и теряются. Префикс алиасов — `CTXG_` (`CTXG_COMPANY_A`, `CTXG_PERSON_3`): валидный идентификатор, не ломает код, коллизии с реальным текстом крайне маловероятны. --- ## Движок детекции Три семейства, `detect.py`: 1. **Словарные** (детерминированные, из `entities.json`). Одна скомпилированная альтернация, отсортированная от длинных к коротким. Устойчивость строится прямо в regex, без карт офсетов: классы гомоглифов (`[аa]`, `[еe]`, `[оo]`, `[рp]`, `[сc]`) против обхода смешанными алфавитами, гибкость по пробелам и дефисам, регистронезависимость. Русская морфология — per-entity режим `exact | stem | regex`; `stem` даёт `Петров(?:[а-яё]{0,4})?\b`, покрывая падежи. 2. **Паттерные (секреты).** Курируемый набор: AWS `AKIA/ASIA`, GitHub `ghp_/gho_/ghs_/github_pat_`, Slack `xox[baprs]-`, Stripe `sk_live/rk_live`, Google `AIza`, OpenAI `sk-proj-`, Anthropic `sk-ant-`, JWT `eyJ`, PEM-блоки, SSH-ключи, `postgres://user:pass@`, generic `(api[_-]?key|secret|token|password)\s*[:=]\s*…{16,}`. Плюс Луна для карт и РФ-специфика: ИНН, ОГРН, СНИЛС, паспорт, `+7`-телефоны, email. FP-фильтры: энтропийный порог (Шеннон > 3.5) для generic-правил и allow-list заведомо фейковых значений (`AKIAIOSFODNN7EXAMPLE`, `example.com`, `sk-test-*`), иначе хук будет блокировать документацию. 3. **Структурные.** По пути: `.env*`, `*.pem`, `id_rsa*`, `credentials`, `~/.aws`, `~/.ssh`, `.git-credentials`. По команде: `env`, `printenv`, `aws configure get`, `gh auth token`, `kubectl get secret -o yaml`, `docker inspect`, `op read`, `pass show`. **Самозащита** — отдельная группа deny, без неё всё остальное декоративно: агент не может писать в `settings.json`, `settings.local.json`, `hooks/hooks.json`, `scripts/ctxguard/**` и `~/.claude/ctx-guard/**`. Иначе он выключит собственный контроль. --- ## Чтобы агент не забывал Четыре слоя, по убыванию силы. Порядок принципиален: первый слой делает забывание безвредным, остальные лишь сохраняют агента полезным. 1. **Механическое принуждение.** Хук блокирует независимо от того, что агент помнит. Память — оптимизация UX, граница безопасности — хук. 2. **Переинъекция на `SessionStart`** с матчерами `startup|resume|clear|compact|fork`: компактный блок правил (цель — ≤400 токенов) плюс глоссарий алиасов. В глоссарий уходит **только** сторона алиасов и безопасное поле `hint` («`CTXG_COMPANY_A` — ритейл-заказчик», «`CTXG_PERSON_3` — бэкендер»). Агент получает смысл без значения. Переживает компактинг. 3. **Коррекция в момент ошибки.** Каждый `deny` несёт `permissionDecisionReason`, который повторяет правило и даёт готовое действие: «используй `CTXG_COMPANY_A`; если это новая сущность — `/ctx-entity add`». Обучение в точке ошибки работает лучше любого баннера. При подстановке — `additionalContext`, объясняющий, почему путь изменился. 4. **`CLAUDE.md`** — две строки-указатель, чтобы правило выживало, если плагин отключили, плюс триггероёмкий `description` у скилла. Напоминания на `UserPromptSubmit` дозируем: одна строка, только если недавно было нарушение. Баннер в каждом промпте выжигает внимание и токены. --- ## Проверяемость (без неё «эффективно» — это вера) - **`ctxguard verify`** — канареечный корпус: подаём на реальные точки входа хуков настоящий hook-JSON на stdin и проверяем решение. Кейсы: фейковый AWS-ключ; компания; русское ФИО в шести падежах; обход гомоглифами; секрет внутри base64; секрет в JSON в выводе Bash; попытка записи в `settings.json`. Это одновременно регрессионный тест и метрика. - **`ctxguard scan-transcript`** — измерение по факту, а не по замыслу. Транскрипт `~/.claude/projects//*.jsonl` — это буквально то, что было отправлено. Грепаем его по словарю и паттернам, считаем реальные утечки за сессию. Вешаем на `SessionEnd`. - **`ctxguard audit`** — jsonl 0600: `ts, session, tool, rule_id, alias, action`. Без plaintext. - **`ctxguard verify --adversarial`** — сабагент пытается вынести канарейку (приём из `superpowers/skills/writing-skills/testing-skills-with-subagents.md`). --- ## План работ **Фаза 0 — каркас репозитория.** `marketplace.json`, оба `plugin.json`, `.gitignore`, `Makefile`, `README.md`, скелет плагина-«ведра». Коммит. Риска нет. **Фаза 1 — движок и тесты, без хуков.** `policy.py`, `store.py` (словарь, 0600), `detect.py`, `morph.py`, `substitute.py`, юнит-тесты, канареечный корпус, `verify`, `scan-transcript`. TDD (`superpowers:test-driven-development`). Хуки не подключены → нулевой риск, но уже появляется базовая метрика утечек на текущих транскриптах. **Фаза 2 — калибровка.** Прогон движка в режиме наблюдения по истории транскриптов и репозиторию, чистка false positives, наполнение allow-list. Это шаг разработки, а не режим поставки: в проде — fail-closed, как решено. **Фаза 3 — слой запрета.** `hooks.json` + `pretooluse.py`: структурные deny (пути, команды), секреты, самозащита, MCP/Task fail-closed. Уже здесь система реально защищает. **Фаза 4 — слой подстановки.** `ctxguard run` (обёртка Bash), двойники для `Read`/`Grep`, `pathmap`, обратная гидратация `Write`/`Edit`. *Перед реализацией — короткий спайк:* обёртка Bash не должна сломать персистентный шелл Claude Code. Проверить проброс кода возврата, `cd`/`export`/`source` (их не оборачиваем — вывода у них нет, но строку команды всё равно сканируем), построчную буферизацию и `run_in_background`. Это единственное место с неизвестным поведением рантайма. **Фаза 5 — слой памяти.** `sessionstart.py` (инъекция правил + глоссарий алиасов), `userpromptsubmit.py` (детект + exit 2 + подсказка), тексты `permissionDecisionReason`, строки для `CLAUDE.md`. **Фаза 6 — оформление.** `SKILL.md`, четыре `references/*.md`, слэш-команды (`/ctx-entity`, `/ctx-sanitize`, `/ctx-verify`, `/ctx-audit`), `--adversarial`, README. --- ## Что эта система честно не закрывает Пишем это в `references/threat-model.md` первым разделом, а не мелким шрифтом. - **Псевдонимизация ≠ анонимизация.** По структуре проекта, доменам в конфигах и именам пакетов заказчик часто восстанавливается. Если требование юридическое или контрактное, правильный контроль — zero-retention договор или self-hosted развёртывание, а обфускация лишь снижает объём утечки. - **Скриншоты и изображения** не фильтруются. Текст на картинке уходит как есть. - **История git не переписывается.** Реальные имена остаются в объектах; их ловит фильтр вывода при чтении, но сам репозиторий остаётся «грязным». - **Собственный вывод модели** не контролируется: агент может воспроизвести реальное имя, если вывел его из контекста. - **`~/.claude/settings.local.json` уже содержит `Bash(python3 *)`** в allow. Хуки это не обходит (они срабатывают всегда), но человек из цикла подтверждения выпадает. Стоит сузить. - **Первый прогон до наполнения словаря защищает слабо.** Словарные детекторы знают только то, что в них внесли; секретные и структурные работают сразу. --- ## Верификация 1. `make test` — юнит-тесты движка (детекция, морфология, гомоглифы, подстановка, pathmap). 2. `make verify` → `ctxguard verify`: таблица по канареечному корпусу, все кейсы `PASS`. Отдельно проверить, что запись в `settings.json` заблокирована. 3. Ручной e2e в отдельном каталоге-песочнице: положить файл с фейковым ключом и фейковой компанией, попросить агента прочитать файл и запустить `env`, `cat .env`, `git log`. Ожидаемо: чтение отдано двойником, секрет — ``, компания — `CTXG_COMPANY_A`, `env` и `cat .env` — deny с внятной причиной. 4. `ctxguard scan-transcript` по транскрипту этой e2e-сессии → ожидается **ноль** реальных значений. Это финальная приёмка: она измеряет то, что действительно ушло. 5. Проверка памяти: `/compact`, затем спросить агента правила — блок правил и глоссарий должны вернуться через `SessionStart` с матчером `compact`. 6. `ctxguard verify --adversarial` — сабагент не смог вынести канарейку.