⚠️ Не медицинское изделие. Не медицинская рекомендация. Некоммерческий проект. Предоставляется «как есть», без гарантий. Использование — на собственный риск. Все демо-данные вымышлены. Полные условия — DISCLAIMER.md (в корне репозитория). 🚨 При неотложном состоянии — скорая помощь.
Модель угроз, устройство защиты и правила работы. Документ описывает не только то, что защищено, но и то, что не защищено, — второе для медицинских данных важнее.
Правила ниже выросли из аудита безопасности рабочей версии системы. Четыре критические уязвимости в дашборде были подтверждены практически: через них читались произвольные файлы с диска, включая конфигурацию с живыми API-ключами, и записывались файлы за пределы проекта. Всё это устранено, и разделы документа объясняют, каким механизмом и почему именно таким.
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 | Ничего исходящего — только входящие метрики | Только если интеграция включена |
Отсюда два практических вывода:
- «Данные не покидают устройство» относится к хранению, а не к обработке. Файлы лежат локально, а рассуждает над ними модель в облаке.
- Каждая MCP-интеграция расширяет периметр. Todoist и Calendar опциональны именно поэтому: их можно не включать, и система от этого не сломается. Решение осознанное, и принимаете его вы.
Привычный .gitignore перечисляет, что скрыть. Здесь наоборот: скрывается всё, а исключения называются поимённо.
Data/**
!Data/**/
!Data/**/.gitkeep
!Data/**/README.md
!Data/**/*.example.json
!Data/**/*.demo.json
!Data/labs/_marker-aliases.json
!Data/specialists/*.jsonВ любой схеме ошибки случаются. Вопрос в том, куда они ведут.
При обычном подходе («перечисляем, что скрыть») забытая строка означает, что файл с медданными попадает в git. Заметить это можно спустя месяцы, а история git постоянна: удаление файла новым коммитом не убирает его из прошлых.
При инвертированном подходе забытая строка означает, что нужный служебный файл не попадёт в репозиторий. Это обнаруживается сразу — при первом клонировании чего-то не хватает — и чинится добавлением одной строки. Ущерб нулевой.
Оба варианта ошибаются одинаково часто. Но первый ошибается в сторону утечки, а второй — в сторону неудобства.
Неочевидная деталь, без которой конструкция не работает. Git не заглядывает внутрь проигнорированного каталога: если каталог исключён целиком, разыгнорить файл внутри него невозможно, сколько бы исключений вы ни написали.
!Data/**/ разыгноривает сами каталоги, оставляя игнорируемыми их файлы. Только после этого начинают действовать точечные исключения для шаблонов и справочников.
Проверить, что предохранитель работает:
echo '{}' > Data/__probe.json
git check-ignore -v Data/__probe.json # должно вывести правило
rm Data/__probe.jsonТу же проверку setup.sh выполняет автоматически при каждом запуске и сообщает об ошибке, если Data/ вдруг оказался открыт.
Cache/, Archive/, Inbox/ и Goals/ закрыты тем же способом — содержимое игнорируется, остаются только .gitkeep. Это не формальность: Cache/ хранит активный контекст и сессионные логи, которые по концентрации медицинской информации не уступают самим данным.
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. Они остаются на диске, под теми же правами доступа, что и остальные данные.
Дашборд — единственный компонент системы, который слушает сеть, и потому единственный, у которого есть настоящая поверхность атаки.
"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.
Роуты вида /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 приемлемо «ровно потому, что сервер доступен только с этой машины». Это рассуждение было неверным, и его стоит разобрать: ошибка типична.
Привязка к 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 работают.
| Правило | Почему |
|---|---|
.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-ключ, используйте их.
Вероятность и ущерб оценены для типового сценария: одна личная машина, один пользователь, локальный репозиторий без remote.
| Сценарий | Вероятность | Ущерб | Текущая защита | Достаточна |
|---|---|---|---|---|
| Кража устройства в выключенном состоянии | низкая | критический | Шифрование диска средствами ОС — FileVault, LUKS, BitLocker | да, если шифрование включено |
| Кража устройства включённым и разблокированным | низкая | критический | Нет второго рубежа: данные лежат в открытом виде | нет |
Случайный git push из проекта |
очень низкая | критический | Remote отсутствует физически; Data/ под .gitignore; setup.sh проверяет оба условия |
да |
| Публикация репозитория с недочищенными данными | средняя | критический | Инвертированный .gitignore, автопроверка в setup.sh |
частично — нужен ручной чек-лист, блок 8 |
| Шаринг скриншота с открытой IDE | средняя | высокий | Технической нет. Помогает только то, что медданные лежат в Data/, а не в корне |
нет |
| Доступ другого пользователя ОС к машине | низкая | высокий | chmod -R go-rwx Data, .mcp.json — 600 |
да для непривилегированных пользователей |
| Физический доступ к разблокированной машине | низкая | критический | Никакой: права на файлы защищают от других учётных записей, а не от вашей | нет |
| Дашборд запущен в публичной сети | средняя | критический | Привязка к 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. Она не защищает их от человека, у которого уже есть доступ к вашей разблокированной машине.
Прямой список, чтобы не строить ложных ожиданий.
Нет шифрования на уровне приложения. Файлы в Data/ — обычный JSON, CSV и Markdown в открытом виде. Любой процесс, работающий под вашей учётной записью, читает их без препятствий. Конфиденциальность в состоянии покоя целиком полагается на шифрование диска средствами операционной системы. Если оно выключено — включите, это единственная реальная защита при потере устройства.
# macOS
fdesetup status
# Linux (LUKS)
lsblk -o NAME,FSTYPE,MOUNTPOINT | grep cryptНет аудита доступа. Система не ведёт журнал того, кто, когда и что прочитал. Установить постфактум, были ли данные скопированы, невозможно.
Нет многопользовательского режима. Один каталог — один человек. Нет ролей, нет разграничения, нет разделения данных членов семьи. Права доступа устроены так, чтобы данные читал только владелец каталога, и это единственный уровень разграничения.
Нет защиты от скомпрометированной машины. Вредоносный процесс под вашей учётной записью получает всё: данные, токены, историю. Ни один механизм проекта этому не противостоит.
Нет сертификации и соответствия регуляторным требованиям. Health-OS — персональный инструмент, а не медицинская информационная система. HIPAA, GDPR как обработчик, ISO 27001 — ничего из этого не заявляется и не подразумевается. Для профессионального использования с чужими данными система не предназначена.
Нет резервного копирования. Локальный git защищает от ошибочного редактирования, но не от потери диска. Резервные копии — ваша задача, и делать их нужно в зашифрованное хранилище.
# 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Данные вымышленного пациента выглядят так же, как настоящие, и позволяют показать всё, включая консилиум.
Порядок действий, от быстрого к долгому.
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.
Перед тем как принять новый роут, скрипт или скилл, работающий с файлами:
- Строится ли путь из пользовательского ввода. Если да — используется ли
resolveWithin, а неpath.join. - Валидируется ли ввод до записи. Тип, обязательность, enum, календарная корректность даты, физиологический диапазон.
- Не расширяет ли изменение поверхность доступа наружу. Новый слушающий сокет, новый исходящий запрос, новая интеграция — всё это меняет модель угроз, и её нужно перечитать.
- Не появляются ли данные пациента в местах, где их быть не должно: в промптах агентов, в коммитах, в логах, в названиях задач внешних сервисов.
- Не ослабляет ли изменение
.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 Claude Code — он изолирует файловую систему и сеть для команд оболочки на уровне операционной системы, а не инструкций.
По умолчанию он выключен. Включается командой /sandbox или ключом "sandbox": {"enabled": true}. Именно он закрывает обходы через sudo и сторонние интерпретаторы, перечисленные выше.
В настройки проекта он не добавлен намеренно: sandbox меняет поведение оболочки для всей сессии, и это решение пользователя, а не значение по умолчанию, навязанное репозиторием.