Skip to content

Latest commit

 

History

History
440 lines (289 loc) · 42.9 KB

File metadata and controls

440 lines (289 loc) · 42.9 KB

Безопасность

⚠️ Не медицинское изделие. Не медицинская рекомендация. Некоммерческий проект. Предоставляется «как есть», без гарантий. Использование — на собственный риск. Все демо-данные вымышлены. Полные условия — DISCLAIMER.md (в корне репозитория). 🚨 При неотложном состоянии — скорая помощь.

Модель угроз, устройство защиты и правила работы. Документ описывает не только то, что защищено, но и то, что не защищено, — второе для медицинских данных важнее.

Правила ниже выросли из аудита безопасности рабочей версии системы. Четыре критические уязвимости в дашборде были подтверждены практически: через них читались произвольные файлы с диска, включая конфигурацию с живыми API-ключами, и записывались файлы за пределы проекта. Всё это устранено, и разделы документа объясняют, каким механизмом и почему именно таким.


Блок 1. Принцип: данные не покидают устройство

Health-OS исходит из того, что медицинские данные человека не должны уходить с его машины. Это архитектурное решение, а не настройка, которую можно включить или выключить.

Разница существенная. Настройку забывают включить, отключают «на время», теряют при переустановке. Архитектурное решение работает иначе: чтобы его нарушить, нужно совершить осознанное действие, которое видно.

Как это выражено в конструкции:

Механизм Что делает Почему именно так
Репозиторий без remote Пушить некуда Не «нельзя пушить», а физически некуда. Барьер не полагается на дисциплину
Инвертированный .gitignore Игнорируется всё содержимое Data/, исключения перечислены поимённо Ошибка приводит к тому, что файл не попадёт в git, а не к утечке
Оригиналы вне контроля версий PDF, сканы, DICOM не индексируются git Они содержат PHI в сыром виде и переживают в истории любое удаление
Дашборд на loopback 127.0.0.1, без доступа из сети У дашборда нет аутентификации, и она не нужна, пока он недоступен извне
Промпты без PII Ни один агент не содержит данных пациента Промпт устаревает, данные обновляются; факты живут только в Data/
Права 600 и go-rwx Данные и секреты читает только владелец Второй пользователь системы или чужой процесс не получит доступ по умолчанию

Что при этом всё-таки уходит наружу

Честная оговорка, без которой предыдущая таблица вводила бы в заблуждение. Система построена вокруг облачной модели, и при обычной работе часть данных передаётся вовне.

Куда Что уходит Когда
Anthropic API Содержимое Data/, попадающее в контекст: анализы, профиль, история визитов При любом обращении к скиллу или агенту. Это основной канал — консилиум по определению передаёт медданные модели
Todoist Названия и описания задач: планы обследований, специальности врачей Только если интеграция включена
Google Calendar Названия событий: даты и типы визитов Только если интеграция включена
WHOOP Ничего исходящего — только входящие метрики Только если интеграция включена

Отсюда два практических вывода:

  1. «Данные не покидают устройство» относится к хранению, а не к обработке. Файлы лежат локально, а рассуждает над ними модель в облаке.
  2. Каждая MCP-интеграция расширяет периметр. Todoist и Calendar опциональны именно поэтому: их можно не включать, и система от этого не сломается. Решение осознанное, и принимаете его вы.

Блок 2. Инвертированный .gitignore

Привычный .gitignore перечисляет, что скрыть. Здесь наоборот: скрывается всё, а исключения называются поимённо.

Data/**

!Data/**/
!Data/**/.gitkeep
!Data/**/README.md
!Data/**/*.example.json
!Data/**/*.demo.json
!Data/labs/_marker-aliases.json
!Data/specialists/*.json

Почему направление ошибки важнее её вероятности

В любой схеме ошибки случаются. Вопрос в том, куда они ведут.

При обычном подходе («перечисляем, что скрыть») забытая строка означает, что файл с медданными попадает в git. Заметить это можно спустя месяцы, а история git постоянна: удаление файла новым коммитом не убирает его из прошлых.

При инвертированном подходе забытая строка означает, что нужный служебный файл не попадёт в репозиторий. Это обнаруживается сразу — при первом клонировании чего-то не хватает — и чинится добавлением одной строки. Ущерб нулевой.

Оба варианта ошибаются одинаково часто. Но первый ошибается в сторону утечки, а второй — в сторону неудобства.

Зачем строка !Data/**/

Неочевидная деталь, без которой конструкция не работает. Git не заглядывает внутрь проигнорированного каталога: если каталог исключён целиком, разыгнорить файл внутри него невозможно, сколько бы исключений вы ни написали.

!Data/**/ разыгноривает сами каталоги, оставляя игнорируемыми их файлы. Только после этого начинают действовать точечные исключения для шаблонов и справочников.

Проверить, что предохранитель работает:

echo '{}' > Data/__probe.json
git check-ignore -v Data/__probe.json    # должно вывести правило
rm Data/__probe.json

Ту же проверку setup.sh выполняет автоматически при каждом запуске и сообщает об ошибке, если Data/ вдруг оказался открыт.

Что находится вне Data/

Cache/, Archive/, Inbox/ и Goals/ закрыты тем же способом — содержимое игнорируется, остаются только .gitkeep. Это не формальность: Cache/ хранит активный контекст и сессионные логи, которые по концентрации медицинской информации не уступают самим данным.


Блок 3. Оригиналы документов

PDF, сканы, фотографии и DICOM исключены из контроля версий по расширениям:

*.pdf
*.dcm
*.jpg
*.jpeg
*.png
*.heic
*.tif
*.tiff
*.dicom

Причин две, и обе весомые.

Первая: концентрация PHI. Структурированный Data/labs/2026-03-15_cbc.json содержит числа и названия маркеров. Исходный бланк той же лаборатории содержит ФИО, дату рождения, номер полиса, адрес подразделения, печать и подпись врача. Разбирая документ, система забирает клиническое содержание и оставляет за бортом идентифицирующую обвязку — при условии, что сам бланк не попадёт в репозиторий.

Вторая: необратимость. Git хранит историю. Файл, удалённый сегодня, лежит во всех коммитах, где он был. Очистка истории технически возможна, но это операция с переписыванием всех хешей, и уверенности в полноте она не даёт: копии остаются в reflog, в упакованных объектах, в клонах, если они успели появиться. Проще не допускать, чем вычищать.

Единственное исключение — Dashboard/public/**, иконки и статика интерфейса. Медицинского содержания там нет по определению.

Оригиналы при этом никуда не деваются: /inbox переносит их в Archive/, который тоже вне git. Они остаются на диске, под теми же правами доступа, что и остальные данные.


Блок 4. Дашборд

Дашборд — единственный компонент системы, который слушает сеть, и потому единственный, у которого есть настоящая поверхность атаки.

Только loopback

"dev": "next dev --turbopack -H 127.0.0.1",
"start": "next start -H 127.0.0.1"

По умолчанию Next.js слушает 0.0.0.0 — все интерфейсы. В аудите это подтвердилось практически: запрос к /api/profile с другого устройства в той же Wi-Fi-сети возвращал полный медицинский профиль. Кофейня, коворкинг, гостевая сеть — везде, где вы открываете ноутбук, ваш медпрофиль был бы доступен соседям.

Флаг -H 127.0.0.1 закрывает это целиком: сокет привязан к петлевому интерфейсу, снаружи к нему подключиться нельзя независимо от того, что происходит в коде приложения.

Аутентификации у дашборда нет, и пока он на loopback, она не нужна. Но если вы когда-нибудь решите открыть доступ извне — через смену -H, через туннель, через reverse proxy, — аутентификация становится обязательной первым делом, до всего остального.

Проверить текущую привязку:

lsof -nP -iTCP:3000 -sTCP:LISTEN

Должен быть 127.0.0.1:3000, а не *:3000.

Path traversal и resolveWithin

Роуты вида /api/labs/[file] принимают имя файла из URL. Наивная реализация склеивает его с каталогом данных:

path.join(LABS_DIR, filename)     // так делать нельзя

path.join не защищает. Он схлопывает .., но результат спокойно выходит за пределы базового каталога: path.join("/Data/labs", "../../../.claude.json") вернёт путь к файлу в домашнем каталоге. В аудите через это читались конфигурации с живыми API-ключами и записывались файлы за пределы проекта — вплоть до возможности подменить хук и добиться выполнения произвольной команды.

Правильный резолв делает единственный хелпер resolveWithin в Dashboard/lib/data/utils.ts. Каждая из его проверок закрывает конкретный обход:

export function resolveWithin(
  baseDir: string,
  filename: string,
  allowedExtensions?: string[]
): string {
  // 1. Декодирование — иначе %2F..%2F проходит мимо проверки на разделители
  const decoded = decodeURIComponent(filename);

  // 2. Нулевой байт — обрезает путь на уровне системного вызова
  if (decoded.includes("\0")) throw new Error("invalid filename: null byte");

  const base = path.resolve(baseDir);
  const target = path.resolve(base, decoded);

  // 3. Разделитель в конце обязателен: без него /Data/labs-secret
  //    пройдёт проверку на префикс /Data/labs
  if (target !== base && !target.startsWith(base + path.sep)) {
    throw new Error("path escapes base directory");
  }

  // 4. Белый список расширений — .md-роут не должен отдавать .env
  if (allowedExtensions?.length) { /* ... */ }

  return target;
}

Правило для любого нового кода: путь, в построении которого участвует пользовательский ввод, резолвится только через resolveWithin. Прямой path.join с внешними данными — дефект, а не стилистическое предпочтение.

При чтении исключение гасится и превращается в 404. При записи оно пробрасывается намеренно — попытка записать файл за пределы каталога данных должна падать громко, а не молча делать вид, что всё в порядке.

Валидация ввода

Второй слой — Dashboard/lib/data/validation.ts. Он решает другую задачу: не пустить в данные то, что их испортит.

  • isPlainFilename отвергает параметр [file], содержащий разделители пути или переходы вверх, ещё до обращения к файловой системе. resolveWithin такой путь тоже отверг бы, но исключением — наружу уходила бы пятисотка с внутренним текстом ошибки. Отказ во входных данных — это 400, а не сбой сервера.
  • Validator проверяет типы, обязательность, enum-значения, календарную корректность дат и то, что дата не из будущего. Ошибки копятся и возвращаются разом.
  • RANGES задаёт грубые физиологические границы: вес 8.25 вместо 82.5 и гемоглобин 15.8 вместо 158 не попадут в тренд. Это не диагностика, а защита от опечатки в разряде.
  • Слияние вместо замены. Тело PUT-запроса накладывается на существующий файл, а не заменяет его: иначе редактор, не знающий о поле pdf_path или studies[], стирал бы их при каждом сохранении.
  • Поле version никогда не сбрасывается — на него опирается миграция схем.

Чего у дашборда нет

  • Ограничения размера тела запроса. Гигантский POST исчерпает память процесса.
  • Аудита доступа. Кто и что читал — нигде не фиксируется.

Оба пункта приемлемы потому, что сервер доступен только с этой машины. Меняя это условие, вы меняете и вывод.

CSRF — было и стало

Раньше здесь стояло, что отсутствие защиты от CSRF приемлемо «ровно потому, что сервер доступен только с этой машины». Это рассуждение было неверным, и его стоит разобрать: ошибка типична.

Привязка к loopback защищает от злоумышленника в сети. Против CSRF она не даёт ничего, потому что браузер жертвы работает на той же машине: страница, открытая в соседней вкладке, обращается к 127.0.0.1:3000 и попадает в тот же сервер. Локальность здесь не смягчающее обстоятельство, а необходимое условие атаки.

Уязвимость была подтверждена практически. Запрос

PUT /api/profile
Origin: https://evil.example
Content-Type: text/plain

возвращал {"success":true} и затирал блок basic в профиле — дату рождения, пол, рост, экстренный контакт. Без даты рождения ломается возрастная логика, педиатрический режим и все возрастные референсы. Тип text/plain выбран не случайно: он относится к «простым» запросам и не вызывает preflight, то есть браузер отправляет его без предварительного разрешения сервера.

Изменяющих роутов тринадцать, Origin не проверял ни один.

Закрыто в Dashboard/middleware.ts — одна точка на все роуты /api/*:

Вектор Проверка
CSRF из вкладки браузера У изменяющих методов сверяются Origin и Sec-Fetch-Site. Браузер проставляет их сам, подделать со страницы нельзя
DNS rebinding Домен злоумышленника, резолвящийся в 127.0.0.1, обходит привязку к loopback, но приходит с чужим Host — он проверяется отдельно

Запрос без Origin и без Sec-Fetch-Site пропускается: это не браузер, а curl или скрипт. CSRF-вектором он не является, а тот, кто уже исполняет команды на машине, в обходе дашборда не нуждается.

Проверено: атака выше и ещё три вектора получают 403, легитимные запросы дашборда с 127.0.0.1 и с localhost работают.


Блок 5. Ключи и секреты

Правило Почему
.mcp.json — вне git, права 600 Файл содержит токены. 600 закрывает его от других пользователей системы и от процессов, работающих не под вашим аккаунтом
Ключи никогда не попадают в промпты, задачи и коммиты Всё, что попало в промпт, ушло в облако. Всё, что попало в коммит, останется в истории
Токены сервисов — в конфигурации самих серверов, а не размазаны по файлам Один файл с секретами проще защитить и проще отозвать
.env, *.key, *.pem, credentials.json — в .gitignore Дешёвая страховка от привычки положить файл «на минуточку» в корень проекта

Проверить права:

ls -l .mcp.json                  # ожидается -rw-------
find Data -type f ! -perm 600 | head

Отдельно стоит проверить конфигурацию Claude Code в домашнем каталоге. Она хранит переменные окружения MCP-серверов в открытом виде, и по умолчанию её права могут быть 644 — то есть файл читает любой пользователь системы и любой процесс:

ls -l ~/.claude.json
chmod 600 ~/.claude.json

Это файл вне проекта, setup.sh его не трогает, но именно он был целью эксплуатации path traversal в аудите.

Пароли вместо токенов — отдельная плохая идея. Токен отзывается в один клик и ограничен по правам; пароль открывает аккаунт целиком. Если интеграция позволяет OAuth или API-ключ, используйте их.


Блок 6. Модель угроз

Вероятность и ущерб оценены для типового сценария: одна личная машина, один пользователь, локальный репозиторий без remote.

Сценарий Вероятность Ущерб Текущая защита Достаточна
Кража устройства в выключенном состоянии низкая критический Шифрование диска средствами ОС — FileVault, LUKS, BitLocker да, если шифрование включено
Кража устройства включённым и разблокированным низкая критический Нет второго рубежа: данные лежат в открытом виде нет
Случайный git push из проекта очень низкая критический Remote отсутствует физически; Data/ под .gitignore; setup.sh проверяет оба условия да
Публикация репозитория с недочищенными данными средняя критический Инвертированный .gitignore, автопроверка в setup.sh частично — нужен ручной чек-лист, блок 8
Шаринг скриншота с открытой IDE средняя высокий Технической нет. Помогает только то, что медданные лежат в Data/, а не в корне нет
Доступ другого пользователя ОС к машине низкая высокий chmod -R go-rwx Data, .mcp.json600 да для непривилегированных пользователей
Физический доступ к разблокированной машине низкая критический Никакой: права на файлы защищают от других учётных записей, а не от вашей нет
Дашборд запущен в публичной сети средняя критический Привязка к 127.0.0.1 в dev и start да, пока -H не изменён
Вредоносная вкладка браузера обращается к 127.0.0.1 средняя высокий Path traversal закрыт, ввод валидируется, изменяющие запросы сверяют Origin и Sec-Fetch-Site да
DNS rebinding: чужой домен резолвится в 127.0.0.1 низкая высокий Заголовок Host проверяется на петлевой интерфейс да
Передача данных в облако через MCP и агентов достоверность 100 % средний Осознанный компромисс: интеграции опциональны, канал задокументирован частично — управляется вашим выбором
Компрометация токена интеграции средняя средний Права 600, токены вне git частично — отзыв токена остаётся ручной операцией
Утечка через Cache/ и Archive/ при шаринге каталога средняя высокий Оба вне git, но на диске лежат в открытом виде нет при копировании каталога целиком

Три строки со значением «нет» объединяет общее: это не дефекты реализации, а границы модели. Система защищает данные от сети и от git. Она не защищает их от человека, у которого уже есть доступ к вашей разблокированной машине.


Блок 7. Что система не защищает

Прямой список, чтобы не строить ложных ожиданий.

Нет шифрования на уровне приложения. Файлы в Data/ — обычный JSON, CSV и Markdown в открытом виде. Любой процесс, работающий под вашей учётной записью, читает их без препятствий. Конфиденциальность в состоянии покоя целиком полагается на шифрование диска средствами операционной системы. Если оно выключено — включите, это единственная реальная защита при потере устройства.

# macOS
fdesetup status

# Linux (LUKS)
lsblk -o NAME,FSTYPE,MOUNTPOINT | grep crypt

Нет аудита доступа. Система не ведёт журнал того, кто, когда и что прочитал. Установить постфактум, были ли данные скопированы, невозможно.

Нет многопользовательского режима. Один каталог — один человек. Нет ролей, нет разграничения, нет разделения данных членов семьи. Права доступа устроены так, чтобы данные читал только владелец каталога, и это единственный уровень разграничения.

Нет защиты от скомпрометированной машины. Вредоносный процесс под вашей учётной записью получает всё: данные, токены, историю. Ни один механизм проекта этому не противостоит.

Нет сертификации и соответствия регуляторным требованиям. Health-OS — персональный инструмент, а не медицинская информационная система. HIPAA, GDPR как обработчик, ISO 27001 — ничего из этого не заявляется и не подразумевается. Для профессионального использования с чужими данными система не предназначена.

Нет резервного копирования. Локальный git защищает от ошибочного редактирования, но не от потери диска. Резервные копии — ваша задача, и делать их нужно в зашифрованное хранилище.


Блок 8. Чек-лист перед тем, как поделиться

Перед публикацией или передачей репозитория

# 1. Ничего из рабочих каталогов не отслеживается
git ls-files | grep -E '^(Data|Cache|Archive|Inbox|Goals)/'
# Ожидается: только *.example.*, *.demo.*, README.md, .gitkeep
#            и справочники Data/labs/_marker-aliases.json, Data/specialists/*.json

# 2. Оригиналов документов нет ни в рабочем дереве, ни в истории
git ls-files | grep -iE '\.(pdf|jpe?g|png|heic|tiff?|dcm|dicom)$'
git log --all --pretty=format: --name-only --diff-filter=A \
  | sort -u | grep -iE '\.(pdf|jpe?g|png|heic|dcm)$'
# Ожидается: пусто, кроме Dashboard/public/

# 3. Секретов нет
git ls-files | grep -E '(\.mcp\.json|\.env|\.key|\.pem|credentials\.json)$'
git log --all -p | grep -inE 'sk-[a-z0-9]{10}|ghp_|Bearer [A-Za-z0-9]{20}' | head

# 4. Абсолютных путей с вашим именем пользователя нет
grep -rIn "$HOME" --exclude-dir=node_modules --exclude-dir=.git . | head

# 5. Персональных данных в отслеживаемых файлах нет
git ls-files -z | xargs -0 grep -lniE 'фамилия|полис|снилс|дата рождения' | head

Если хотя бы одна проверка что-то нашла — не публикуйте очищенный репозиторий, создайте новый. История git постоянна, и вычистить её надёжнее, чем начать заново, обычно не получается. Правильный порядок: новый каталог, git init, копирование только кода, первый коммит.

Перед скриншотом

  • Закройте вкладки дашборда с профилем, анализами и визитами.
  • Сверните дерево файлов IDE или уберите из кадра — имена файлов в Data/labs/ сами по себе рассказывают, чем вы болеете.
  • Проверьте, что в кадре нет Cache/active-context.md и MEMORY.md: там текущие диагнозы и планы обследований в концентрированном виде.
  • Проверьте историю терминала в кадре — команды часто содержат имена файлов с датами и специальностями.
  • Панель уведомлений и заголовок окна тоже попадают в кадр.

Перед демонстрацией экрана

Разверните демо-набор в отдельном каталоге и показывайте его:

git clone <репозиторий> /tmp/health-os-demo
cd /tmp/health-os-demo && ./setup.sh --demo

Данные вымышленного пациента выглядят так же, как настоящие, и позволяют показать всё, включая консилиум.


Блок 9. Если есть подозрение на утечку

Порядок действий, от быстрого к долгому.

1. Оцените канал. Что именно могло уйти: репозиторий, отдельный файл, скриншот, доступ к дашборду из сети, токен интеграции. От этого зависит всё остальное.

2. Отзовите токены — первым делом. Это единственное действие, которое действительно обратимо, и единственное, где скорость имеет значение.

  • Todoist: Settings → Integrations → Developer → отозвать и выпустить новый.
  • Google: страница управления доступом аккаунта → отозвать доступ приложения.
  • Anthropic и прочие сервисы — через их консоли.
  • После отзыва обновите .mcp.json и проверьте права: chmod 600 .mcp.json.

3. Если утёк git-репозиторий. Удалите его из публичного доступа, но не считайте это решением: форки, клоны и кеши поисковых систем остаются. Исходите из того, что попавшее в публичный репозиторий разошлось. Практический вывод — не пытайтесь «почистить историю и вернуть», создайте новый репозиторий с нуля и опубликуйте только код.

4. Если дашборд был доступен из сети. Проверьте привязку (lsof, блок 4), верните -H 127.0.0.1. Установить, обращался ли кто-то, невозможно — аудита доступа нет. Исходите из худшего: считайте, что содержимое Data/ могло быть прочитано, а токены из конфигураций — скомпрометированы, и отзовите их.

5. Если утёк отдельный файл данных. Медицинские данные нельзя «отозвать» — в этом их принципиальное отличие от пароля. Реалистичное действие: понять объём — что именно было в файле, кто мог получить, чем это грозит практически. Для большинства бытовых сценариев ущерб репутационный, а не операционный, но решение о том, кого уведомлять, принимаете вы.

6. Проверьте, не повторится ли. Прогоните чек-лист из блока 8 целиком и ./setup.sh — он проверит изоляцию репозитория и работу .gitignore.


Блок 10. Правила для тех, кто дописывает код

Перед тем как принять новый роут, скрипт или скилл, работающий с файлами:

  1. Строится ли путь из пользовательского ввода. Если да — используется ли resolveWithin, а не path.join.
  2. Валидируется ли ввод до записи. Тип, обязательность, enum, календарная корректность даты, физиологический диапазон.
  3. Не расширяет ли изменение поверхность доступа наружу. Новый слушающий сокет, новый исходящий запрос, новая интеграция — всё это меняет модель угроз, и её нужно перечитать.
  4. Не появляются ли данные пациента в местах, где их быть не должно: в промптах агентов, в коммитах, в логах, в названиях задач внешних сервисов.
  5. Не ослабляет ли изменение .gitignore. Любое новое исключение в разделе Data/ требует объяснения, почему этот файл заведомо не содержит личных данных.

⚕️ Документ описывает защиту данных, а не медицинские решения. Для решений о лечении обратитесь к врачу.


Права агента

До версии с профилями .claude/settings.json разрешал Bash, Write, Edit, WebFetch и WebSearch без подтверждения, а список deny был пуст. Для репозитория с медицинскими файлами это чрезмерно: агент, читающий присланный PDF, мог выполнить любую команду оболочки.

Сейчас права минимальные, и устроены они так:

Список Что в нём Зачем
allow Чтение, поиск, запись только в Data/, Cache/, Archive/, Inbox/, Goals/, чтение страниц с девяти медицинских доменов Обычная работа не требует подтверждений
ask Bash, WebSearch Каждая команда оболочки и каждый поиск — с вашего разрешения
deny Запись в .claude/**, .git/**, чтение ~/.ssh, ~/.claude, ~/.aws, ~/.gnupg, любых .env*, .mcp.json; curl, wget, nc, ssh, scp, rsync, git push, git remote add Барьер, работающий при любых настройках

Ключевое свойство: deny и явные ask действуют во всех режимах, включая режим полного обхода разрешений. allow в этом режиме не значит ничего — поэтому реальную защиту дают именно два других списка. Запрет на запись в .claude/** означает, что агент не может расширить собственные права: это тот случай, где правило защищает само себя.

Чего эти правила не делают

Перечислено честно, потому что защита, границы которой не названы, опаснее её отсутствия.

  • Обёртки обходят запреты Bash. Правило Bash(curl:*) блокирует curl …, но не sudo curl …, не bash -c "curl …", не python3 -c "import urllib…". Claude Code снимает перед сверкой лишь фиксированный набор обёрток — timeout, nice, nohup и подобные, — и sudo в него не входит. Настоящий барьер здесь — ask на Bash: любая нераспознанная команда всё равно спросит разрешения
  • Запреты на файлы не видят сторонних процессов. Read(**/.env*) останавливает чтение через встроенные инструменты и распознаваемые команды вроде cat, но не Python-скрипт, открывающий файл сам
  • Edit-запреты не покрывают Bash. git commit и rm через оболочку .git и .claude затронуть могут — иначе сломались бы обычные операции с репозиторием
  • Bash(git push:*) хрупок: лишний пробел или вызов по абсолютному пути к бинарнику могут не совпасть с шаблоном. Реальный предохранитель — отсутствие remote у репозитория по построению
  • Хуки сессий не проходят через эти правила — они запускаются оболочкой вне тул-вызовов, поэтому deny на .claude/** их не ломает

Sandbox

Для работы с реальными медицинскими данными стоит включить встроенный sandbox Claude Code — он изолирует файловую систему и сеть для команд оболочки на уровне операционной системы, а не инструкций.

По умолчанию он выключен. Включается командой /sandbox или ключом "sandbox": {"enabled": true}. Именно он закрывает обходы через sudo и сторонние интерпретаторы, перечисленные выше.

В настройки проекта он не добавлен намеренно: sandbox меняет поведение оболочки для всей сессии, и это решение пользователя, а не значение по умолчанию, навязанное репозиторием.