Keeps credentials, company names, personal names and PII out of the model's context. Enforcement lives in Claude Code hooks rather than in instructions to the agent: a skill alone cannot protect anything, because by the time the agent reads a rule the surrounding context has already been sent. Credentials are removed irreversibly and marked. Entities from a user-supplied dictionary become stable aliases, rewritten back to real values on their way to disk and to the shell, so code and commands referring to them still work. Published from a clean tree; development history is not included.
22 KiB
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; секреты гасим безвозвратно (метка
<SECRET:type:hash>). - Периметр: ввод/вывод инструментов и содержимое репозитория.
- Упаковка: плагин 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}
└── <bucket>-skills/ # «ведро» для простых скиллов без хуков
├── .claude-plugin/plugin.json
└── skills/<name>/SKILL.md
Правило размещения: скиллу нужны хуки, команды или MCP → отдельный плагин в plugins/.
Скилл — это чистые инструкции → каталог внутри plugins/<bucket>/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/<slug>/entities.json # словарь real↔alias, 0600
├── projects/<slug>/index.json # кеш «в каком файле есть сущности» (по mtime+size)
├── cache/<slug>/… # санитизированные двойники файлов, 0700
└── audit.jsonl # 0600, только alias/rule_id, без plaintext
Два жёстких инварианта:
- Весь
~/.claude/ctx-guard/в deny-read для агента, иBashс упоминанием этого пути тоже. Иначе агент прочитает словарь и одним чтением сольёт всё сразу. - В CLI нет ни одной ветки кода, печатающей реальное значение сущности. Только алиасы, типы и хеши. Это делает утечку через сам инструмент невозможной, а не маловероятной.
Матрица перехвата
Всё через PreToolUse, один хук-владелец на инструмент (иначе гонка updatedInput).
| Поверхность | Механизм | Enforced |
|---|---|---|
| Промпт пользователя | UserPromptSubmit: детект → exit 2 + причина с подсказкой /ctx-sanitize |
да, блоком |
| Строка команды Bash | updatedInput.command — обратная подстановка alias→real |
да |
| Вывод Bash | updatedInput.command → python3 ctxguard run -- <cmd>: обёртка сама запускает команду и стримит отфильтрованный вывод |
да |
Содержимое 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:
- Словарные (детерминированные, из
entities.json). Одна скомпилированная альтернация, отсортированная от длинных к коротким. Устойчивость строится прямо в regex, без карт офсетов: классы гомоглифов ([аa],[еe],[оo],[рp],[сc]) против обхода смешанными алфавитами, гибкость по пробелам и дефисам, регистронезависимость. Русская морфология — per-entity режимexact | stem | regex;stemдаётПетров(?:[а-яё]{0,4})?\b, покрывая падежи. - Паттерные (секреты). Курируемый набор: AWS
AKIA/ASIA, GitHubghp_/gho_/ghs_/github_pat_, Slackxox[baprs]-, Stripesk_live/rk_live, GoogleAIza, OpenAIsk-proj-, Anthropicsk-ant-, JWTeyJ, 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-*), иначе хук будет блокировать документацию. - Структурные. По пути:
.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/**. Иначе он выключит собственный контроль.
Чтобы агент не забывал
Четыре слоя, по убыванию силы. Порядок принципиален: первый слой делает забывание безвредным, остальные лишь сохраняют агента полезным.
- Механическое принуждение. Хук блокирует независимо от того, что агент помнит. Память — оптимизация UX, граница безопасности — хук.
- Переинъекция на
SessionStartс матчерамиstartup|resume|clear|compact|fork: компактный блок правил (цель — ≤400 токенов) плюс глоссарий алиасов. В глоссарий уходит только сторона алиасов и безопасное полеhint(«CTXG_COMPANY_A— ритейл-заказчик», «CTXG_PERSON_3— бэкендер»). Агент получает смысл без значения. Переживает компактинг. - Коррекция в момент ошибки. Каждый
denyнесётpermissionDecisionReason, который повторяет правило и даёт готовое действие: «используйCTXG_COMPANY_A; если это новая сущность —/ctx-entity add». Обучение в точке ошибки работает лучше любого баннера. При подстановке —additionalContext, объясняющий, почему путь изменился. CLAUDE.md— две строки-указатель, чтобы правило выживало, если плагин отключили, плюс триггероёмкийdescriptionу скилла.
Напоминания на UserPromptSubmit дозируем: одна строка, только если недавно было нарушение.
Баннер в каждом промпте выжигает внимание и токены.
Проверяемость (без неё «эффективно» — это вера)
ctxguard verify— канареечный корпус: подаём на реальные точки входа хуков настоящий hook-JSON на stdin и проверяем решение. Кейсы: фейковый AWS-ключ; компания; русское ФИО в шести падежах; обход гомоглифами; секрет внутри base64; секрет в JSON в выводе Bash; попытка записи вsettings.json. Это одновременно регрессионный тест и метрика.ctxguard scan-transcript— измерение по факту, а не по замыслу. Транскрипт~/.claude/projects/<slug>/*.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. Хуки это не обходит (они срабатывают всегда), но человек из цикла подтверждения выпадает. Стоит сузить.- Первый прогон до наполнения словаря защищает слабо. Словарные детекторы знают только то, что в них внесли; секретные и структурные работают сразу.
Верификация
make test— юнит-тесты движка (детекция, морфология, гомоглифы, подстановка, pathmap).make verify→ctxguard verify: таблица по канареечному корпусу, все кейсыPASS. Отдельно проверить, что запись вsettings.jsonзаблокирована.- Ручной e2e в отдельном каталоге-песочнице: положить файл с фейковым ключом и фейковой
компанией, попросить агента прочитать файл и запустить
env,cat .env,git log. Ожидаемо: чтение отдано двойником, секрет —<SECRET:…>, компания —CTXG_COMPANY_A,envиcat .env— deny с внятной причиной. ctxguard scan-transcriptпо транскрипту этой e2e-сессии → ожидается ноль реальных значений. Это финальная приёмка: она измеряет то, что действительно ушло.- Проверка памяти:
/compact, затем спросить агента правила — блок правил и глоссарий должны вернуться черезSessionStartс матчеромcompact. ctxguard verify --adversarial— сабагент не смог вынести канарейку.