Add ctxguard: hook-enforced context sanitization
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.
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
# ctx-tools — как пользоваться
|
||||
|
||||
**Маркетплейс плагинов для Claude Code.** Сейчас в нём один плагин:
|
||||
|
||||
**`ctxguard`** — санитизация контекста: вырезает креденшлы, подменяет имена компаний,
|
||||
людей и PII на стабильные обратимые алиасы до того, как они попадут в модель.
|
||||
Принуждение живёт в хуках, поэтому агент не может это выключить.
|
||||
|
||||
Этот файл — про то, как этим пользоваться. Раздача плагина коллегам и процесс
|
||||
релизов — в [README.en.md](README.en.md).
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
### Вариант 1. Через маркетплейс (рекомендуется)
|
||||
|
||||
```
|
||||
/plugin marketplace add /path/to/skills
|
||||
/plugin install ctxguard@ctx-tools
|
||||
```
|
||||
|
||||
Вместо локального пути можно указать `<owner>/<repo>` на GitHub, git-URL или любой
|
||||
путь в файловой системе. Ничего не тянется из сети во время работы, зависимостей кроме
|
||||
`python3` нет.
|
||||
|
||||
Для всей команды — объявить на уровне **проекта** и закоммитить результат, чтобы
|
||||
клонирования репозитория было достаточно:
|
||||
|
||||
```
|
||||
claude plugin marketplace add <owner>/<repo> --scope project
|
||||
claude plugin install ctxguard@ctx-tools --scope project
|
||||
```
|
||||
|
||||
### Вариант 2. Копированием папки
|
||||
|
||||
```
|
||||
cp -r plugins/ctxguard ~/.claude/skills/ctxguard
|
||||
```
|
||||
|
||||
Загрузится в следующей сессии как `ctxguard@skills-dir`, **вместе с хуками**. Два
|
||||
нюанса:
|
||||
|
||||
- **Имя должно быть свободно.** Установленный плагин с тем же именем побеждает, и
|
||||
просто *отключить* его недостаточно — имя остаётся занятым до
|
||||
`claude plugin uninstall`.
|
||||
- Копируйте каталог, а не симлинк на репозиторий, который потом переедет.
|
||||
|
||||
Этот способ — для одной машины или одного коллеги. Маркетплейс — когда одну и ту же
|
||||
версию нужно раздать нескольким людям и обновление должно быть командой, а не
|
||||
перекопированием.
|
||||
|
||||
---
|
||||
|
||||
## ctxguard: повседневное использование
|
||||
|
||||
Ниже `ctxguard` — сокращение для
|
||||
|
||||
```
|
||||
python3 plugins/ctxguard/scripts/ctxguard.py
|
||||
```
|
||||
|
||||
Удобно завести алиас:
|
||||
`alias ctxguard='python3 /path/to/skills/plugins/ctxguard/scripts/ctxguard.py'`.
|
||||
Внутри сессии Claude Code тот же скрипт вызывается как
|
||||
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/ctxguard.py"`.
|
||||
|
||||
### Что он делает
|
||||
|
||||
- **Креденшлы удаляются безвозвратно** — на их месте остаётся маркер вида
|
||||
`SECRET:правило:хэш` в угловых скобках. Восстановить значение нельзя, и это
|
||||
осознанно: отредактированный приватный ключ бесполезен.
|
||||
- **Имена и PII превращаются в стабильные алиасы** (`CTXG_COMPANY_A`,
|
||||
`CTXG_PERSON_B`, `CTXG_EMAIL_7F3A`) — и переводятся обратно в реальные значения по
|
||||
пути на диск и в шелл. Поэтому агент продолжает делать реальную работу: код и
|
||||
команды, ссылающиеся на алиасы, работают.
|
||||
- **Принуждение живёт в хуках, а не в скилле.** Агент не может это выключить, а то,
|
||||
что он забыл правила, безвредно — решение принимает хук.
|
||||
|
||||
Хуки: `SessionStart`, `UserPromptSubmit`, `PreToolUse` (перехват до запуска
|
||||
инструмента), `PostToolUse` (обратный перевод при записи), `SessionEnd`.
|
||||
|
||||
### Первый запуск в новом проекте
|
||||
|
||||
```
|
||||
ctxguard init # создать состояние, показать что активно
|
||||
ctxguard scan . # что реально лежит в этом репозитории (только счётчики)
|
||||
ctxguard entity add "Acme Corp" --type company --hint "розничный клиент"
|
||||
ctxguard verify # 43 канареечных кейса через настоящие точки входа хуков
|
||||
ctxguard scan-transcript # что реально дошло до модели
|
||||
```
|
||||
|
||||
**Словарь стартует пустым.** Детект креденшлов и PII работает сразу; имена компаний,
|
||||
людей и хостов защищены только после регистрации — поэтому `scan`, а затем
|
||||
`entity add` — первая работа в новом репозитории.
|
||||
|
||||
Словарь привязан к **корню git-репозитория**, а не к текущему каталогу, так что
|
||||
сессия, начатая в `src/`, использует тот же словарь.
|
||||
|
||||
### Как выбрать `--match` — от этого зависит, скрывает ли алиас хоть что-то
|
||||
|
||||
| Режим | Когда | Что ловит |
|
||||
|---|---|---|
|
||||
| `exact` (по умолчанию) | имя встречается только в прозе | целые слова, терпит гомоглифы (кириллическая `с` вместо латинской `c`) и разные разделители: `Acme Corp` / `acme-corp` / `AcmeCorp` |
|
||||
| `stem` | всё, что склоняется — в первую очередь русские фамилии | плюс до четырёх символов окончания на слово: `Петров / Петрова / Петровым`. Перебирает по замыслу: `Петров` поймает и `Петровский` |
|
||||
| `ident` | **любое имя, встречающееся в коде** | без границ слова, так что значение находится внутри идентификаторов: `ContosoClient`, `CONTOSO_API_KEY`, `contoso.rs`. С `exact` все три утекут в реальном написании, выглядя защищёнными. Нужно ≥4 символов |
|
||||
| `regex` | точная настройка | используется как есть, без именованных групп |
|
||||
|
||||
Побеждает самый длинный шаблон, так что регистрировать и `Acme`, и
|
||||
`Acme Corporation` безопасно.
|
||||
|
||||
Ещё два флага:
|
||||
|
||||
- `--hint` вставляется в контекст модели **буквально**, поэтому описывайте сущность,
|
||||
не называя её («розничный клиент», а не «Globex — наш клиент»). Хинт, содержащий
|
||||
само значение, отвергается.
|
||||
- `--variant` (можно повторять) — дополнительные написания: транслитерации,
|
||||
аббревиатуры.
|
||||
|
||||
Если у значения несколько слов, а режим `ident`, — зарегистрируйте отдельно и
|
||||
отличительное слово: код обычно пишет `GlobexClient` для `Globex Retail`.
|
||||
|
||||
### Слэш-команды в сессии
|
||||
|
||||
| Команда | Зачем |
|
||||
|---|---|
|
||||
| `/ctx-entity <значение> [тип]` | Защитить название компании, человека, хост или кодовое имя за стабильным алиасом |
|
||||
| `/ctx-sanitize` | Превратить текст (тикет, лог, письмо) в версию с алиасами, которую можно куда-то вставить |
|
||||
| `/ctx-audit` | Показать, что ctxguard блокировал и подменял |
|
||||
| `/ctx-verify` | Доказать, что защита действительно блокирует заявленное, и измерить реальную утечку |
|
||||
|
||||
Промпт нельзя перезаписать хуком — только заблокировать. Поэтому если ваш промпт
|
||||
отклонён, штатный путь — `/ctx-sanitize` на текст, ручная подстановка алиасов или
|
||||
регистрация значения через `/ctx-entity`.
|
||||
|
||||
### CLI
|
||||
|
||||
| Команда | Что делает |
|
||||
|---|---|
|
||||
| `ctxguard init [--force]` | создать каталог состояния и политику по умолчанию |
|
||||
| `ctxguard status` | режим, префикс алиасов, число сущностей, правил, записей аудита |
|
||||
| `ctxguard mode enforce\|observe` | `observe` только логирует находки и ничего не блокирует — средство калибровки, не режим эксплуатации |
|
||||
| `ctxguard entity add <значение> --type <тип> [--match …] [--hint …] [--variant …]` | защитить значение. Типы: `company`, `person`, `host`, `project`, `email`, `phone`, `custom` |
|
||||
| `ctxguard entity list` | алиасы, типы, режимы совпадения, хинты — **значения не печатаются никогда** |
|
||||
| `ctxguard entity remove <алиас>` | снять защиту |
|
||||
| `ctxguard entity export --out <файл>` | выгрузить словарь для коллеги (откажется писать внутрь git-репозитория) |
|
||||
| `ctxguard entity import <файл>` | влить словарь, выгруженный на другой машине |
|
||||
| `ctxguard sanitize < <файл>` | stdin → текст с алиасами на stdout, сводка по правилам на stderr |
|
||||
| `ctxguard scan [путь…]` | какие файлы содержат чувствительные значения (только счётчики и id правил) |
|
||||
| `ctxguard scan-transcript [--limit N] [--verbose] [--all-projects]` | что реально дошло до модели |
|
||||
| `ctxguard audit -n 40` | последние решения: id правил, имена инструментов, алиасы — без плейнтекста |
|
||||
| `ctxguard verify [--adversarial]` | канареечный корпус через настоящие точки входа хуков |
|
||||
| `ctxguard run -- <команда>` | выполнить команду с фильтрацией вывода |
|
||||
|
||||
Ни одна подкоманда не печатает реальное значение сущности или секрета — именно это
|
||||
делает сам инструмент безопасным для запуска изнутри сессии агента.
|
||||
|
||||
### Через Makefile
|
||||
|
||||
```
|
||||
make test # юнит-тесты движка
|
||||
make verify # канареечный корпус через настоящие точки входа хуков
|
||||
make leaks # поиск в транскриптах значений, которые реально дошли до модели
|
||||
make scan # чувствительные значения в этом репозитории (только счётчики)
|
||||
make status # что у ctxguard активно
|
||||
make lint # байт-компиляция всех скриптов + валидация JSON
|
||||
make all # lint + test + verify
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что видит агент — и что делать, когда вызов отклонён
|
||||
|
||||
Пять правил, которые агент получает в контекст:
|
||||
|
||||
1. `CTXG_*` — алиасы реальных значений, использовать **буквально**.
|
||||
2. Никогда не восстанавливать реальное значение и не просить пользователя его
|
||||
вставить.
|
||||
3. Маркер секрета необратим; записывать его в файл нельзя — это затрёт настоящий
|
||||
креденшл заглушкой, и хук такую запись отклонит. Нужно целиться в более узкий
|
||||
участок через Edit.
|
||||
4. Некоторые пути ведут в санитизированный кэш («twin»). Читать и править как
|
||||
обычно — изменение прописывается в настоящий файл за вас. Незнакомый путь — это
|
||||
ожидаемо и само по себе сигнал, что в файле что-то было.
|
||||
5. Отказ — это информация, а не препятствие. В причине названо правило и штатный
|
||||
путь; попытки обойти сами попадают в аудит.
|
||||
|
||||
| Отказ | Штатный путь |
|
||||
|---|---|
|
||||
| Чтение `.env`, `*.pem`, `~/.ssh/…` | Читать `.env.example` или задокументированную схему. Проверить, что переменная задана, не печатая её: `test -n "$VAR" && echo set` |
|
||||
| Команды, чья задача — напечатать креденшл (дамп окружения, выдача токена, чтение секрета кластера) | Проверять наличие, не печатая значение |
|
||||
| `Grep` с `output_mode: content` | `Bash` + `grep` (этот путь фильтруется), либо `Grep` с `files_with_matches`, а затем `Read` |
|
||||
| Поиск реального защищённого значения | Искать по алиасу — в санитизированных копиях лежат алиасы, а не реальные значения |
|
||||
| Креденшл в командной строке | Передать через переменную окружения или файл, который команда читает сама |
|
||||
| `cd` / `export` / `source` с алиасом | Они выполняются в постоянном шелле и не могут быть обёрнуты фильтром; перестроить команду или выполнить шаг вручную |
|
||||
| Аргумент MCP-инструмента с защищённым значением | Использовать алиас; если инструменту нужно реальное значение — этот шаг выполняет пользователь |
|
||||
|
||||
Отдельно про изменение файлов: если правка не появилась в настоящем файле —
|
||||
write-through упал и сообщил об этом (`PostToolUse` выходит с кодом 2 и печатает
|
||||
`write-through to <path> FAILED`). Проверьте права на целевой файл и
|
||||
`ctxguard audit -n 20`. Подготовленное содержимое цело в twin-копии.
|
||||
|
||||
---
|
||||
|
||||
## Проверка, что защита работает
|
||||
|
||||
Два разных вопроса — две разные команды, и путать их нельзя:
|
||||
|
||||
```
|
||||
ctxguard verify # работает ли механика? -> измеряет замысел
|
||||
ctxguard scan-transcript # утекло ли что-то реально? -> измеряет результат
|
||||
```
|
||||
|
||||
Доказательство — только второе: оно ищет реальные значения из словаря в
|
||||
транскриптах, то есть буквально в том, что было отправлено модели. Ненулевой
|
||||
результат означает, что данные дошли до модели несмотря на хуки — тогда сначала
|
||||
смотрите файлы инструкций (`CLAUDE.md` / `AGENTS.md` грузит харнесс, их не видит ни
|
||||
один хук), затем `ctxguard audit`.
|
||||
|
||||
По умолчанию сканируются транскрипты **только этого проекта**: сессии других
|
||||
проектов оцениваются против словаря, который им не принадлежит. Расширить —
|
||||
`--all-projects`.
|
||||
|
||||
Часть находок может быть помечена как **структурные**: значение входит в собственный
|
||||
путь проекта, поэтому появляется в каждом абсолютном пути и в метаданных транскрипта.
|
||||
Никаким хуком это не лечится — только переименованием каталога.
|
||||
|
||||
---
|
||||
|
||||
## Передача словаря другой машине или коллеге
|
||||
|
||||
Плагин путешествует через маркетплейс. Словарь — нет, и не должен.
|
||||
`~/.claude/ctx-guard/` живёт на машине и намеренно вне любого репозитория. Передавать
|
||||
его нужно явно, потому что **алиасы невозможно воспроизвести**:
|
||||
|
||||
- они выдаются в порядке регистрации, так что добавивший две компании в другом
|
||||
порядке получит перевёрнутое соответствие — два человека будут понимать под
|
||||
`CTXG_COMPANY_A` *разные компании*;
|
||||
- соль генерируется на хранилище, поэтому маркеры секретов и PII-алиасы тоже не
|
||||
совпадут, и находки нельзя сопоставить между людьми.
|
||||
|
||||
На это есть тест, так что рекомендация не может тихо устареть.
|
||||
|
||||
```
|
||||
ctxguard entity export --out ~/team-dict.json # откажется писать внутрь git-репозитория
|
||||
# передавать по каналу, по которому вы отправили бы сами реальные значения — файл содержит их
|
||||
ctxguard entity import team-dict.json # запускать из каталога проекта
|
||||
# удалить файл выгрузки сразу после импорта
|
||||
```
|
||||
|
||||
После импорта обе машины дают побайтово одинаковый вывод — включая маркеры и
|
||||
PII-алиасы. Если в проекте уже есть словарь с другой солью, импорт откажется:
|
||||
принять чужую соль (`--adopt-salt`) значит поменять все уже используемые PII-алиасы и
|
||||
маркеры секретов.
|
||||
|
||||
---
|
||||
|
||||
## Чего ctxguard не покрывает
|
||||
|
||||
- `CLAUDE.md` и `AGENTS.md` грузит сам харнесс — **ни один хук их не видит**.
|
||||
Санитизировать вручную; `SessionStart` предупредит, если в них найдены реальные
|
||||
имена.
|
||||
- Структурные утечки: имя, входящее в путь проекта, защитить нельзя.
|
||||
- Пока сущности не зарегистрированы или не импортированы, живы только детект
|
||||
креденшлов и PII — имена компаний, людей и хостов не защищены.
|
||||
|
||||
Подробнее — `plugins/ctxguard/skills/context-sanitization/references/threat-model.md`.
|
||||
Прочитайте его прежде, чем обещать кому-либо, что данные в безопасности.
|
||||
|
||||
Необязательно, но полезно — чтобы ожидание пережило отключение плагина, добавьте в
|
||||
свой `CLAUDE.md`:
|
||||
|
||||
```markdown
|
||||
Чувствительные данные в этом проекте заменены алиасами (`CTXG_*`), это обеспечивают
|
||||
хуки ctxguard. Используй алиасы буквально, никогда не восстанавливай реальные
|
||||
значения, а причину отказа читай вместо того, чтобы его обходить. См. скилл
|
||||
context-sanitization.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Настройка и отладка
|
||||
|
||||
Политика: `~/.claude/ctx-guard/policy.json`. Отсутствует или битая — применяются
|
||||
строгие значения по умолчанию, потому что умолчания и есть безопасный откат.
|
||||
Хранится как diff от умолчаний, чтобы исправления правил доходили до установленных
|
||||
копий.
|
||||
|
||||
Ключевые поля: `mode` (`enforce` / `observe`), `fail_closed` (ошибка санитайзера
|
||||
отклоняет вызов инструмента), `alias_prefix`, `entropy_threshold`, `max_scan_bytes`,
|
||||
`bash_wrap` (выключение убирает фильтрацию вывода шелла целиком — не отключайте ради
|
||||
скорости), `deny_read_paths`, `deny_command_patterns`, `allowlist`,
|
||||
`disabled_secret_rules`. Полный список — `references/policy.md`.
|
||||
|
||||
**Ложные срабатывания** — в порядке предпочтения: точное значение в `allowlist` →
|
||||
конкретный маркер в `allowlist_substrings` → сужение правила в `detect.py` с тестом →
|
||||
id правила в `disabled_secret_rules`. Сначала посмотрите `ctxguard audit`: правило,
|
||||
которое постоянно срабатывает на безобидном, — это причина, по которой защиту в итоге
|
||||
выключают.
|
||||
|
||||
**Стоимость:** порядка 134 мс на решение по Bash и 155 мс на Read, который должен
|
||||
просканировать файл и материализовать twin. На задачу из 50 вызовов инструментов —
|
||||
примерно 6–7 секунд. Twin-копии кэшируются и инвалидируются по mtime+size, так что
|
||||
повторные чтения дешёвые.
|
||||
|
||||
**Правка плагина не подействовала.** Хуки исполняют **установленную копию**, а не
|
||||
рабочее дерево, и `plugin.json` сравнивается по версии, а не по коммиту — релиз без
|
||||
бампа версии не доедет до установленной копии молча. После каждого изменения:
|
||||
|
||||
```
|
||||
claude plugin marketplace update ctx-tools
|
||||
claude plugin update ctxguard@ctx-tools # нужен рестарт сессии
|
||||
```
|
||||
|
||||
Убедиться:
|
||||
`diff -rq ~/.claude/plugins/cache/ctx-tools/ctxguard/<версия>/scripts plugins/ctxguard/scripts`.
|
||||
|
||||
**Выключить.** `/plugin uninstall ctxguard@ctx-tools`. `mode observe` оставляет детекторы
|
||||
и аудит, но ничего не блокирует — это калибровка, а не способ работать защищённо.
|
||||
Состояние в `~/.claude/ctx-guard/` в обоих случаях остаётся; удалите этот каталог,
|
||||
чтобы убрать и словарь.
|
||||
|
||||
---
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
```
|
||||
.claude-plugin/marketplace.json манифест маркетплейса
|
||||
plugins/ctxguard/ санитизация контекста: скилл + хуки + CLI
|
||||
docs/specs/ проектный документ (исторический)
|
||||
Makefile единственный вход в проверки
|
||||
```
|
||||
|
||||
### Куда положить новый скилл
|
||||
|
||||
| Скиллу нужны… | Куда |
|
||||
|---|---|
|
||||
| хуки, слэш-команды, MCP-серверы, скрипты | отдельный плагин в `plugins/<имя>/` + запись в `marketplace.json` |
|
||||
| ничего, кроме инструкций | плагин-«ведро»: `plugins/<bucket>/skills/<имя>/SKILL.md` |
|
||||
|
||||
Хуки не могут ехать внутри голого скилла — именно поэтому репозиторий сделан
|
||||
маркетплейсом, а не плоским каталогом `SKILL.md`. Скиллы «только инструкции» стоит
|
||||
собирать в один плагин-ведро: одна установка покрывает все, тогда как плагины,
|
||||
ставящие хуки, добавляются осознанно и по одному. Такого ведра здесь пока нет —
|
||||
создайте его вместе с первым таким скиллом, а не заранее.
|
||||
|
||||
Конвенции, вслед за экосистемой установленных плагинов:
|
||||
|
||||
- frontmatter в `SKILL.md` — `name` + `description`, третье лицо, много триггеров
|
||||
- детали уходят в `references/*.md`, не в `SKILL.md`
|
||||
- скрипты — Python 3 **только stdlib**, вызов через
|
||||
`python3 "${CLAUDE_PLUGIN_ROOT}/..."`
|
||||
|
||||
Начать чтение стоит с `plugins/ctxguard/skills/context-sanitization/SKILL.md`.
|
||||
Reference in New Issue
Block a user