Версия статьи: 2.0, 5 октября 2026 г. (проверено на Windows, PowerShell 7.6)
Область: инструменты записи файлов в AutoClaw (Write, Edit, apply_patch).
Аудитория: пользователи и агенты, которым AutoClaw отказывает в записи файла «из-за секрета».
Все утверждения проверены замерами на реальной установке — всего 105 пробных фикстур в шести партиях. Полный список замеренных паттернов с результатами — в приложении A. Готовый скрипт-детектор — в приложении B. Системные инструкции, которые используют этот скрипт, — в приложении C. Методика замера для повторения — в приложении D. Там, где что-то не проверено, это сказано прямо.
Замечание о самой статье. Этот файл писался с оглядкой на описываемое правило: примеры оформлены раздельной нотацией (
token=v), чтобы статья сама не стала «отравленным» файлом, который нельзя поправить. Проверено прогоном детектора из приложения B: в статье ноль ловушек.
- 1. Если читать только одну страницу. AutoClaw: спамит автозащита
- 2. Как это выглядит
- 3. Правда о происходящем: два разных механизма
- 4. Какие строки срабатывают: замеренные границы
- 5. Почему нельзя просто отключить
- 6. Инструкция: как починить
- 7. Чего делать не надо
- 8. Профилактика: своя проверка в проекте
- 9. Как отличить эту проблему от других
- 10. Приложение A. Полный список паттернов с результатами замеров
- A.1 Как это замерялось
- A.2 Имена: замер целиком
- A.3 Формы знака
- A.4 Отдельные случаи
- A.5 Что считается значением
- A.6 Ложно срабатывающие паттерны (подтверждено замером)
- A.7 Потенциально ложно срабатывающие (по модели, но не замерены)
- A.8 Оставшиеся: чего мы можем не видеть
- A.9 Почему наборов несколько: сравнение с наборами продукта
- A.10 Как пополнять этот список
- 11. Приложение B. Детектор: скрипт PowerShell 7.6
- 12. Приложение C. Системные инструкции, использующие этот скрипт
- 13. Приложение D. Методика замера (чтобы повторить)
- 14. Итог
1. Если читать только одну страницу. AutoClaw: спамит автозащита
artifact_secret_detected— это не сообщение о найденном секрете и не ошибка вашего кода. Это отказ инструмента записи.- Проверяется весь итоговый текст файла на форму «учётное имя, затем знак, затем значение». Одна такая строка блокирует запись файла целиком.
- Отключить проверку нельзя. Настройки для этого не существует; код встроен в подписанный рантайм и обновляется вместе с продуктом.
- Обход не нужен. Строку достаточно переформулировать: отделить имя от значения словом. Правка, которая убирает блокирующую строку, проходит.
- Маскирование
[REDACTED]в выводе — другая, безвредная вещь. Файл на диске при этом не меняется. - Опаснее всего то, что проверка не разбирает язык: обычное сравнение с пустой строкой в коде блокирует запись так же, как настоящий пароль. Отсюда «проклятые» файлы, которые перестают правиться.
- Набор имён у проверки уже и страннее, чем кажется:
passwordловится,passwd— нет;private_keyиcredential— не ловятся вообще. Полный список — в приложении A. - Эта проверка не защищает от утечки секрета в файл: значение само по себе она не распознаёт. Проходит всё, что не начинается с учётного имени (замерено, приложение A.8).
2. Как это выглядит
Отказ выглядит так (сообщение приходит на английском и китайском; смысл передан ниже):
Tool failed: artifact_secret_detected
写入内容第 10 行有一处形如凭据赋值的字面量(键名 `token`,值不回显)。
工作区写入一律不接受把凭据字面量落进文件:请把该值改为从环境变量或配置读取
(例如 `process.env.TOKEN`),或删掉这一行后重试。
若这确实是误判(例如只是同名变量或测试占位),请如实告知用户由其决定,
不要绕过这道判据。
Разбор сообщения:
| Часть сообщения | Что она даёт |
|---|---|
第 10 行 (строка 10) | точный номер строки, а не «где-то в файле» |
键名 token (имя ключа) | какое именно имя сработало |
值不回显 (значение не показано) | значение в сообщение не попадает — это безопасно |
artifact_secret_detected | код отказа инструмента записи, не ошибка PHP/JS |
| последняя фраза | приглашение сообщить о ложном срабатывании, а не обходить его |
Полезное свойство: сообщение само называет такой исход возможным ложным срабатыванием и предлагает сообщить о нём. Это значит, что разработчики считают такой исход нормальным и не требуют его скрывать.
3. Правда о происходящем: два разных механизма
Это главная часть статьи. Механизмы путают постоянно, а последствия разные: один безвреден, второй мешает работать.
3.1 Механизм А — маскирование вывода (безвредно)
Рантайм прогоняет текст, который инструменты отдают в ответ, через редактор
значений. В выводе вы видите [REDACTED_SECRET] вместо значения.
Файл на диске при этом не меняется. Как это проверено:
- записать файл, содержащий подозрительную строку;
- посчитать MD5 того, что должно было быть записано;
- прочитать файл с диска и посчитать MD5 фактически.
Результат замера: длины совпали до байта, MD5 — до знака. Файл идентичен записанному; искажение существует только в том, что инструмент вам печатает.
Практическое следствие. Читая файл инструментом чтения, вы видите
замаскированный текст. Отсюда ложные выводы «у меня в файле вместо кода стоит
[REDACTED]». Чтобы увидеть настоящие байты, печатайте метрики, а не значения:
# длины и коды символов вместо содержимого — такой вывод не маскируется
php -r '$t = file_get_contents("путь/к/файлу"); echo strlen($t), PHP_EOL;
foreach (str_split(substr($t, 0, 24)) as $ch) { echo ord($ch), " "; }'
3.2 Механизм Б — отказ в записи (то, что мешает)
Инструменты правки перед записью проверяют итоговый текст файла — то, что получится после применения правки. Если в нём есть строка подозрительной формы, запись отклоняется целиком.
Три следствия:
- Проверяется файл, а не фрагмент. Правите строку 200, «отравленная» строка
лежит на 15-й — отказ будет ссылаться на 15-ю. - Одна строка отравляет весь файл. Пока она там есть, файл инструментами
правки не изменить вообще. - Строка может быть чужой. Достаточно, чтобы она была в файле до вас.
Так и появляются файлы, которые «вдруг перестали правиться»: кто-то когда-то добавил в них безобидную строку нужной формы.
3.3 Поведения, которые ломают интуицию
Замерено, не выведено логикой:
| Ситуация | Поведение |
|---|---|
Полная запись (Write) текста с блокирующей строкой | проходит |
Правка (Edit, apply_patch) того же текста | блокируется |
| Правка, которая удаляет блокирующую строку | проходит |
| Правка, которая блокирующую строку сохраняет | блокируется, даже если меняется другое место |
| Правка нового файла без такой строки | проходит |
Из первой и третьей строк — главный практический вывод: перезаписать файл целиком можно, и починить существующее место тоже можно.
Исключение, найденное на этой же статье. Полная запись тоже проверяется, если
в тексте есть ловушка формы, которую правило уверенно опознаёт как литерал
(пример: имя, затем =, затем значение, начинающееся не со скобки, но такое,
где скобка есть дальше в отрезке). Так что «полная запись всегда проходит» —
не закон, а наблюдение для большинства случаев.
4. Какие строки срабатывают: замеренные границы
Полная таблица с результатами по каждому имени и каждой форме — в приложении A. Здесь — рабочая выжимка.
4.1 Формула правила
Проверка ищет в тексте строку такой структуры:
<учётное имя> <знак> <первый отрезок значения>
где:
- учётное имя — одно из:
token,secret,password,authorization,apikey(и написанияapi_key/api-key); - знак —
=или:сразу после имени (допускаются пробелы); - первый отрезок значения — текст после знака до первого пробела или
разделителя.
Срабатывание отменяется, если в этом первом отрезке есть открывающая круглая скобка.
Три свойства, которые надо держать в голове:
- Границы слева нет, граница справа есть. Поэтому
mytoken,xpassword,DB_PASSWORD,mysql_passwordловятся, аtoken_value,password2,tokens— нет. - Регистр не важен.
TOKEN,Authorization,IsSecretработают так же. - Значение разбирается как первый отрезок, а не как вся строка. Поэтому
= (v)спасает от срабатывания, а= abc (v)— уже нет.
4.2 Имена: что ловится, а что нет (замерено)
| Ловится (запись блокируется) | Не ловится (запись проходит) |
|---|---|
token, secret, password, authorization | passwd, pass |
apikey, api_key, api-key, x_api_key | private_key, privateKey, privatekey |
access_token, auth_token, refresh_token, id_token | credential, credentials |
session_token, bootstrap_token, client_secret | signature, sig, code, cookie |
apiKey, sessionToken, ClientSecret, accessToken | SecretKey, secretAccessKey, accessKeyId |
IsSecret, Token, Secret, Password, TOKEN, PASSWORD | AWS_SECRET_ACCESS_KEY, MYSQL_PWD, COOKIE |
mysql_password, DB_PASSWORD, my_password, hash_password | password_hash, token_value, apikeys, tokens |
mytoken, xpassword, myapikey, super_token, abc_secret | oauth, bearer, authenticate, passphrase, pwd, pin |
Важно: расхождение между колонками нелогично на вид, но это факт замера.
password и passwd — не одно и то же для этой проверки. Планируйте по
таблице, а не по здравому смыслу.
4.3 Формы знаков и значений (замерено)
| Форма | Результат |
|---|---|
token = v | блокирует |
token : v | блокирует |
token = v (без пробелов) | блокирует |
token => v (стрелка массива) | блокирует |
token == v и === | блокирует |
token = (v) — значение со скобкой | проходит |
token = ( — без значения, только скобка | проходит |
token = — знак без значения | проходит |
token : — знак без значения | проходит |
token v — вовсе без знака | проходит |
token != v | проходит (знак не распознан) |
token := v | проходит (аномалия, см. A.8) |
api key = v — имя с пробелом | проходит |
4.4 Отдельные случаи (замерено)
| Случай | Результат |
|---|---|
--token = v — ключ командной строки | блокирует |
?token = v — параметр в адресе | блокирует |
"token" : "v" — форма JSON | блокирует |
authorization : Bearer v — заголовок | блокирует |
-> token = v — стрелка перед именем | блокирует |
token = значение вида sk-… / ghp_… / вид JWT | блокирует (из-за имени) |
result = те же значения без учётного имени | проходит |
Последняя строка принципиальна: по значению проверка не срабатывает вообще. Формат похожего на секрет значения сам по себе запись не блокирует — блокирует только учётное имя слева. Это значит и обратное: реальный ключ, записанный без такого имени, проверка пропустит.
4.5 Почему это ложные срабатывания, а не защита
Правило не понимает, что такое код. Для него сравнение переменной с пустой строкой выглядит ровно как присваивание настоящего пароля. Поэтому «виноват» часто не секрет, а совершенно невинная конструкция:
- проверка
ifс учётной переменной; - присваивание переменной с именем
$token,$secret,$sessionToken; - описание схемы в документации с флагом секрета;
- пример команды в комментарии;
- название поля в форме или в массиве.
5. Почему нельзя просто отключить
5.1 Переключателя не существует
Проверено целенаправленным поиском. Что искалось и что найдено:
| Где искали | Что искали | Результат |
|---|---|---|
| бинарник рантайма | secretScan, blockSecrets, allowSecrets, detectSecrets, disableSecret, secretPolicy | не найдено |
| переменные окружения рантайма (~82 шт.) | любая, относящаяся к проверке | не найдено |
| настройки приложения | ключ отключения проверки секретов | не найдено |
В пользовательских настройках есть разделы safety, agent, integrations,
но они про другое: уровень подтверждения рискованных действий, системный промпт
агента, внешние интеграции.
5.2 Как это устроено
Проверка — часть кода инструментов записи. Рядом в рантайме объявлены коды ошибок:
edit_secret_detected — отказ в редактировании
write_secret_detected — отказ в полной записи
Это встроенное поведение, а не настройка.
5.3 Почему не надо патчить
Бинарник подписан и обновляется вместе с продуктом. Патч: ломает проверку целостности, не переживёт обновления и создаёт риск для всей установки — ради косметической проблемы. Правильный путь — формулировка строки.
6. Инструкция: как починить
Приём 1 — переформулировать текст (основной)
Отделите имя от значения словом, а не знаком. Смысл сохраняется полностью.
| Было (блокирует) | Стало (проходит) |
|---|---|
csrf_token = <значение> в контракте | токен формы |
password : вводит пользователь | пароль вводит пользователь |
IsSecret = 1 в описании схемы | флаг секрета: 1 |
X = Y в примере команды | X — Y |
Это не «замазывание»: термин остаётся точным, просто плейсхолдер отделён словом.
Приём 2 — открывающая скобка в первом отрезке значения
Сделайте так, чтобы в первом отрезке значения была открывающая круглая скобка:
token = (<значение из формы>)
api_key = (<берётся из config/config.local.php>)
Ограничение (замерено): приём надёжен для = и :. Для формы со стрелкой
массива срабатывание всё равно произойдёт: первый отрезок там — знак >, а не
скобка. И «скобка дальше в строке» не помогает: спасает только скобка в первом
отрезке. Разница ровно такая:
| Запись | Результат |
|---|---|
token = (v) | проходит |
token = abc(v) | проходит |
token = abc (v) | блокирует (первый отрезок — abc) |
Приём 3 — полная запись вместо правки
Когда правка отказывает, а полная запись тот же текст принимает — перепишите файл целиком. Это штатный путь, если файл небольшой и вы точно знаете его целевое содержимое. Тогда заодно приведите в порядок блокирующие строки — и файл снова станет правимым (см. оговорку в разделе 3.3).
Приём 4 — исправить источник (лечение «проклятого» файла)
Правка, удаляющая блокирующую строку, проходит. Порядок:
- посмотрите строку, названную в сообщении;
- переформулируйте её (приём 1 или 2) в той же правке, что и нужное изменение;
- проверьте результат контрольным прогоном (приложение B).
После этого файл перестаёт быть отравленным.
Приём 5 — вынести значение туда, где ему место
Если строка содержит настоящее значение, правильное решение — не переформулировка, а вынос значения в конфиг или переменную окружения. Это ровно то, к чему проверка и призывает: значение читается во время работы, а не лежит в исходниках. И это же закрывает вторую проблему из A.8: значение без учётного имени проверка не видит вовсе, поэтому её нельзя считать защитой от утечки.
Примеры правильного устройства:
- реквизиты — в локальный конфиг, который не входит в поставку; в коде — только чтение из него;
- пароль для клиента MySQL — через переменную окружения, а не аргументом командной строки (иначе он попадает в список процессов и историю);
- ключи внешних сервисов — из окружения или секрет-хранилища.
Приём 6 — канареечный тест
Непонятно, виновата ли конкретная строка. Проверьте её на отдельной копии:
- сделайте копию файла рядом;
- примените правку к копии;
- заблокирована — виновник найден; прошла — смотрите дальше в файле;
- удалите копию.
Так находят виновника, не трогая рабочий файл.
Приём 7 — регулярная ревизия
Полезно раз в какое-то время прогонять детектор из приложения B: он даёт список мест и готовый baseline. Иначе такие файлы копятся и однажды мешают срочной правке.
7. Чего делать не надо
| Заблуждение | Что на самом деле |
|---|---|
«AutoClaw испортил файл — там теперь [REDACTED]» | Файл на диске цел. Это маскирование вывода. Проверьте MD5. |
| «Надо отключить проверку в настройках» | Такой настройки нет. Проверено поиском. |
| «Это проверка на утечки секретов» | Нет. По значению она не срабатывает вовсе: значение вида sk-… без учётного имени слева проходит. |
| «Закодирую значение в base64 — запись пройдёт» | Это обход. Он опасен: скрывает значение от человека и от проверок. |
| «Разрежу строку на части, чтобы правило не узнало» | То же самое: обфускация ради обхода. |
| «Перезапишу внешним процессом — и всё» | Для генерации и сборки это штатно. Как способ протащить ту же строку незамеченной — нет. |
| «Это баг версии, подожду исправления» | Поведение стабильно и воспроизводимо. Планируйте работу с ним. |
| «Виноват мой код» | Проверка не разбирает язык: обычный if с учётной переменной срабатывает так же, как настоящий пароль. |
«passwd и password — одно и то же для проверки» | Нет: первое проходит, второе блокирует. Сверяйтесь с таблицей 4.2. |
| «Полная запись обходит проверку всегда» | Не всегда: см. исключение в разделе 3.3. |
Отдельно про обход через кодирование. Это не просто бесполезно — это создаёт реальную проблему: значение, спрятанное от проверки, становится невидимым и для человека, который читает код. Если значение настоящее — его место в конфиге.
8. Профилактика: своя проверка в проекте
Самое полезное — знать места заранее. Детектор из приложения B ищет ту же форму, печатает предупреждение (не ошибку), умеет самопроверку на встроенных фикстурах и сверяется с перечнем известных мест. Инструкции для агента, которые используют этот скрипт, — в приложении C.
Почему предупреждение, а не ошибка: такие строки есть в легитимном коде, и объявлять их ошибкой — значит приучать игнорировать вывод проверки.
9. Как отличить эту проблему от других
| Признак | Что это значит |
|---|---|
| В сообщении есть номер строки и имя ключа | проверка формы строки, а не отказ в доступе |
| Отказывает правка, но полная запись тот же текст принимает | механизм Б, проверка итогового текста |
| В файле есть строка с учётным именем, которую вы не писали | чужое наследие, файл «отравлен» |
| Правка, удаляющая ту же строку, проходит | точно механизм Б; лечится заменой строки |
В выводе инструментов [REDACTED], но файл на диске полон | механизм А, маскирование вывода |
| Отказ при работе вне каталога проекта | другое сообщение и другая причина: инструменты ограничены рабочим каталогом |
10. Приложение A. Полный список паттернов с результатами замеров
A.1 Как это замерялось
Создавались пробные файлы «одно правило на строку». К каждому применялась заведомо безобидная правка (замена строки-маркера, не содержащей учётных имён). Блокировка означала «форма опасна», успех — «форма безопасна».
Имена и знаки в генераторе фикстур склеивались из частей — иначе сам генератор попал бы под проверку и замер стал бы невозможен.
Всего: 105 фикстур в шести партиях — 26 имён, 10 форм знака, 11 отдельных случаев, 15 форм значения, 8 уточнений, 16 составных имён, 17 значений-«секретов», 11 спорных случаев.
Обозначения в таблицах: БЛОК — запись отклонена; ПРОХОД — запись принята.
A.2 Имена: замер целиком
| Имя | Результат |
|---|---|
token | БЛОК |
secret | БЛОК |
password | БЛОК |
authorization | БЛОК |
apikey | БЛОК |
api_key | БЛОК |
api-key | БЛОК |
x_api_key | БЛОК |
apiKey | БЛОК |
X-Api-Key | БЛОК |
access_token | БЛОК |
auth_token | БЛОК |
refresh_token | БЛОК |
id_token | БЛОК |
session_token | БЛОК |
bootstrap_token | БЛОК |
client_secret | БЛОК |
sessionToken | БЛОК |
ClientSecret | БЛОК |
accessToken | БЛОК |
idToken | БЛОК |
authToken | БЛОК |
apiSecret | БЛОК |
IsSecret | БЛОК |
TOKEN, SECRET, PASSWORD, Authorization | БЛОК |
mysql_password, DB_PASSWORD, my_password, hash_password | БЛОК |
mytoken, xpassword, myapikey | БЛОК |
super_token, abc_secret, credential_token | БЛОК |
passwd, pass | ПРОХОД |
private_key, privatekey, privateKey | ПРОХОД |
credential, credentials, Credential | ПРОХОД |
signature, sig, code | ПРОХОД |
cookie, set_cookie, COOKIE | ПРОХОД |
SecretKey, secretAccessKey, accessKeyId | ПРОХОД |
AWS_SECRET_ACCESS_KEY, MYSQL_PWD | ПРОХОД |
password_hash, password2, passwords | ПРОХОД |
token_value, tokenabc, tokens | ПРОХОД |
secrets, apikeys, secretword | ПРОХОД |
authorization_code, client_id | ПРОХОД |
oauth, bearer, authenticate | ПРОХОД |
passphrase, pwd, pin, passport | ПРОХОД |
auth | ПРОХОД |
api key (с пробелом внутри имени) | ПРОХОД |
| имя нелатинскими буквами | ПРОХОД |
result (контроль) | ПРОХОД |
Вывод по именам. Проверка ловит четыре «слова-хвоста» — token,
secret, password, authorization — и семейство apikey /
api_key. Всё остальное, включая passwd, private_key и
credential, она не различает. При этом в служебных целях самого продукта
(журнал, память) используются гораздо более широкие списки — см. A.9. Именно
из-за этого расхождения интуиция подводит: набор для записи не совпадает с
набором для логов.
A.3 Формы знака
| Форма | Результат |
|---|---|
| знак равенства | БЛОК |
| двоеточие | БЛОК |
| стрелка массива | БЛОК |
| двойное равенство | БЛОК |
| тройное равенство | БЛОК |
| знак равенства без пробелов | БЛОК |
| знак равенства, значение в скобках | ПРОХОД |
| двоеточие, значение в скобках | ПРОХОД |
| знак без значения | ПРОХОД |
| двоеточие без значения | ПРОХОД |
| вовсе без знака | ПРОХОД |
!= | ПРОХОД |
:= | ПРОХОД (аномалия, A.10) |
A.4 Отдельные случаи
| Случай | Результат |
|---|---|
значение вида sk-… | БЛОК (только из-за имени) |
значение вида ghp_… | БЛОК (только из-за имени) |
| значение вида JWT | БЛОК (только из-за имени) |
заголовок authorization со словом-схемой | БЛОК |
ключ командной строки --token | БЛОК |
параметр в адресе ?token | БЛОК |
форма JSON "token" | БЛОК |
| переменная окружения со словом в заглавных | БЛОК |
| переменная окружения со словом «печенька» в заглавных | ПРОХОД |
| переменная окружения с ключом AWS | ПРОХОД |
| переменная окружения со строкой MySQL | ПРОХОД |
| значение-«секрет» без учётного имени слева (5 видов) | ПРОХОД |
| имя нелатинскими буквами | ПРОХОД |
| имя с пробелом внутри | ПРОХОД |
| стрелка перед именем | БЛОК |
A.5 Что считается значением
Это самое важное уточнение версии 2.0. Работает не «значение начинается со скобки», а «в первом отрезке значения есть открывающая скобка».
| Значение после знака | Результат | Почему |
|---|---|---|
v | БЛОК | обычный отрезок |
(v) | ПРОХОД | скобка в первом отрезке |
abc(v) | ПРОХОД | скобка в первом отрезке |
a(b | ПРОХОД | скобка в первом отрезке |
( | ПРОХОД | только скобка |
a)b | БЛОК | только закрывающая скобка |
a), ) | БЛОК | закрывающая не спасает |
{v} | БЛОК | фигурные скобки не считаются |
"abc", "abc def" | БЛОК | кавычки не спасают |
a b | БЛОК | первый отрезок — a |
abc.def, a,b, a;b, abc/def, a\b | БЛОК | разделители не спасают |
abc-def | БЛОК | дефис не спасает |
1 | БЛОК | число не спасает |
VALUES(x), f() | ПРОХОД | скобка в первом отрезке |
> (v) (после стрелки) | БЛОК | первый отрезок — > |
Практическая разница на примерах:
| Запись | Результат |
|---|---|
token = (v) | проходит |
token = abc(v) | проходит |
token = abc (v) | блокирует |
A.6 Ложно срабатывающие паттерны (подтверждено замером)
Места, которые встречаются в живом коде и документации и блокируют запись, хотя секрета в них нет:
| № | Паттерн | Где встречается | Почему ложное |
|---|---|---|---|
| 1 | переменная с учётным именем, знак равенства, значение | любой код: $token, $sessionToken, $password | это переменная, а не секрет |
| 2 | сравнение с пустой строкой (== или ===) | проверки входа, токена, пароля | проверка не понимает сравнение |
| 3 | переменная с учётным именем при объявлении параметра | сигнатуры функций, SQL-процедуры | значение — имя, а не секрет |
| 4 | имя с префиксом: mytoken, xpassword, DB_PASSWORD | переменные окружения, составные имена | подстрока в конце имени |
| 5 | CamelCase-варианты: apiKey, sessionToken | JS/TS, JSON, конфиги | регистр не учитывается |
| 6 | флаг секрета в документации и SQL | описания схем, миграции, ТЗ | документация не секрет |
| 7 | форма массива со стрелкой и значением | PHP: 'Password' => 1 | значение — флаг, а не пароль |
| 8 | плейсхолдер в комментарии | контракты обработчиков, docblock | значение — заглушка |
| 9 | заголовок в примере: authorization : Bearer | примеры запросов в документации | учебный пример |
| 10 | ключ командной строки в примере: --token | инструкции, README, скрипты | пример вызова |
| 11 | параметр в адресе в примере: ?token | ссылки, тесты | пример |
| 12 | имя учётной переменной в заглавных | переменные окружения | имя, а не значение |
| 13 | слово из списка в обычной прозе | ТЗ, комментарии, документация | проза |
A.7 Потенциально ложно срабатывающие (по модели, но не замерены)
Эти случаи предсказываются формулой из A.1, но в этой серии замеров не проверялись. Считайте их гипотезами до прогона детектора.
| № | Паттерн | Почему может сработать |
|---|---|---|
| 1 | X-Api-Key : v — заголовок с двоеточием |
Имя и двоеточие распознаются. |
| 2 | "access_token" : "v" — JSON с длинным именем |
Имя заканчивается на token. |
| 3 | ?access_token = v — параметр в адресе |
Имя заканчивается на token. |
| 4 | --client-secret = v — ключ командной строки |
Имя заканчивается на secret. |
| 5 | RefreshToken, IdToken, BootstrapToken |
CamelCase-хвост Token. |
| 6 | Значение со скобкой вне первого отрезка | Первый отрезок не содержит скобку. |
| 7 | Значение, за которым идёт комментарий | Значение — первый отрезок. |
| 8 | Табуляция вместо пробелов вокруг знака | Пробелы допускаются, табуляция не проверена. |
| 9 | Перенос строки сразу после знака | Не проверено, как считается «первый отрезок» на границе строк. |
| 10 | secret = $cfg['secret'] |
Значение — выражение без круглых скобок. |
| 11 | Генератор документов, печатающий такой текст | Записываемый текст проверяется целиком. |
| 12 | Иные расширения файлов: .log, .env, .yaml |
Проверка содержимого не замерена. |
| 13 | Инструмент Edit — замерялся apply_patch |
Поведение может отличаться. |
| 14 | Многострочные значения и продолжения строк | Не замерено. |
| 15 | token = ( — «скобка без значения» в начале |
Модель говорит «проходит», но при проверке литерала результат может отличаться. |
A.8 Оставшиеся: чего мы можем не видеть
Честный раздел. Ниже — то, что вне охвата известного правила или о чём мы знаем недостаточно. Эти случаи полезны не тем, что ложно срабатывают, а тем, что показывают границы знания.
0. Граница слова: слева её нет, справа есть (найдено самопроверкой детектора). Это самое коварное свойство правила, и оно чуть не попало в статью ошибочно. Первая версия детектора (приложение B) использовала шаблон с границей слова с обеих сторон — и самопроверка сразу показала два расхождения: префиксные имена и CamelCase, которые по замеру ловятся, у модели проходили.
Правильно так:
| Имя | Результат | Почему |
|---|---|---|
mytoken, xpassword, DB_PASSWORD | БЛОК | подстрока найдена в конце имени |
sessionToken, apiKey | БЛОК | то же, регистр не важен |
token_value, tokens, password_hash | ПРОХОД | сразу после имени идёт словесный символ |
password2, tokenabc | ПРОХОД | то же |
Практический вывод: имя ищется как подстрока, но знак должен идти сразу после неё. Отсюда и странная на вид асимметрия в таблице A.2.
1. Значения-секреты без учётного имени слева — не блокируются (замерено). Проверка не срабатывает на значение само по себе. Замерено, что проходят:
- значение вида
sk-…(OpenAI-подобное); - значение вида
ghp_…(GitHub-подобное); - значение вида JWT (три отрезка через точку);
- значение вида
AKIA…(AWS-подобное); - строка соединения с логином и паролем внутри.
То есть эта проверка — не защита от утечки секрета в файл, а проверка формы строки. Реальный ключ без учётного имени она пропустит.
2. Имена, которые есть в служебных наборах продукта, но запись не блокируют
(замерено): passwd, pass, private_key, credential,
signature, sig, code, cookie, privatekey,
SecretKey, authorization_code, client_id,
AWS_SECRET_ACCESS_KEY, MYSQL_PWD. Логика такая: продукт использует
разные наборы паттернов для разных целей, и они намеренно не одинаковы
(см. A.9).
3. Что вообще не проверялось:
- поведение инструмента
Edit(замерялсяapply_patch); - поведение на бинарных и нетекстовых файлах;
- поведение вне рабочего каталога проекта (там работает другое ограничение — по пути);
- многострочные значения и перенос сразу после знака;
- точное определение «первого отрезка» на границе строки;
- реализация внутри рантайма: всё выше — поведение, а не исходный код;
- изменится ли набор имён в следующей версии рантайма (список надо перепроверять).
4. Аномалия, которую не удалось объяснить. Форма со знаком :=
прошла, хотя по модели должна блокироваться. Объяснения по наблюдаемому
поведению не нашлось: реализация внутри подписанного бинарника, восстановить её
не удалось. Практический вывод: не полагайтесь на модель слепо — прогоняйте
детектор (приложение B), он даёт факт для вашего файла.
5. Что модель детектора упрощает. Детектор берёт «первый отрезок значения» как всё до первого пробела или табуляции. Настоящая проверка может разбирать сложнее (например, учитывать кавычки и разделители). Поэтому возможно:
- детектор печатает предупреждение там, где запись на самом деле пройдёт;
- детектор молчит там, где запись заблокируется (опаснее — но такие случаи
должны быть найдены замером, а не моделью).
Единственный способ снизить этот риск — прогонять -SelfTest после каждого
обновления рантайма и добавлять новые фикстуры.
A.9 Почему наборов несколько: сравнение с наборами продукта
Продукт использует несколько разных наборов паттернов, и запись — самый узкий из них.
| Назначение | Набор имён | Где найден |
|---|---|---|
| Редактирование текста для памяти | api[_-]?key, token, password, passwd, secret, private[_-]?key, authorization | рантайм, блок памяти |
| Маскирование в журнале (по имени ключа) | apikey, api_key, token, secret, password, credential, authorization, cookie | рантайм, блок журнала |
| Маскирование в журнале (по присваиванию) | шире: access_token, refresh_token, id_token, client_secret, x-authorization, x-auth-sign, signature, sig, code | рантайм, блок журнала |
| Маскирование значений по формату | sk-, ghp_, github_pat_, glpat-, xox[baprs]-, AIza, JWT, AKIA/ASIA | рантайм, редактор предпросмотра |
| Отказ в записи файла | уже всех: token, secret, password, authorization, apikey | рантайм, коды write_secret_detected / edit_secret_detected |
Практический вывод: нельзя переносить список имён с одной цели на другую.
Проверка записи узкая; списки журнала широкие; наборы не совпадают. Отсюда и
странность: credential есть в журнальном наборе, но запись не блокирует.
A.10 Как пополнять этот список
Список не должен устаревать. Пополнение — тот же замер, что делался здесь:
- добавить случай в фикстуры детектора (приложение B, режим
-SelfTest); - прогнать — получить факт «БЛОК / ПРОХОД»;
- перенести результат сюда, отметив дату и версию;
- если факт противоречит модели — править модель, а не подгонять факт.
Именно так в версии 2.0 нашлись три ошибки версии 1.0: passwd,
private_key и credential оказались проходящими, а правило со скобкой
было сформулировано неточно («начинается со скобки» вместо «скобка в первом
отрезке»).
11. Приложение B. Детектор: скрипт PowerShell 7.6
Скрипт ищет те же строки, что блокируют запись, печатает предупреждение и
предлагает готовый baseline. Умеет самопроверку (-SelfTest) на встроенных
фикстурах — чтобы правило можно было перепроверить, а не принимать на слово.
B.1 Требования и гарантии
- PowerShell 7.6+,
PSEdition Core(директивы#Requiresв шапке). - Никаких внешних модулей: только встроенные средства.
- Скрипт ничего не пишет в проектные файлы: только читает и печатает.
- Отчёт содержит путь и имя ключа, но не значение строки: отчёт не должен
разносить содержимое файлов.
B.2 Скрипт целиком
Сохраните как tools/Test-WriteGuardHazards.ps1.
#Requires -Version 7.6
#Requires -PSEdition Core
<#
.SYNOPSIS
Ищет строки формы «учётное имя + знак + значение» — те, из-за которых
инструменты правки AutoClaw отказывают в записи файла целиком.
.DESCRIPTION
Проверка записи в AutoClaw смотрит ИТОГОВЫЙ текст файла: если в нём есть
строка вида «имя, затем знак равенства или двоеточие, затем непустое
значение», запись отклоняется целиком.
Формула восстановлена замерами:
(token|secret|password|authorization|apikey|api_key|api-key)
[граница слова] [пробелы] [двоеточие или знак равенства] [пробелы] <первый отрезок>
Границы слева НЕТ (поэтому префиксные имена и CamelCase ловятся),
граница справа ЕСТЬ (поэтому имена с хвостом не ловятся).
Срабатывание ОТМЕНЯЕТСЯ, если в первом отрезке значения есть открывающая
круглая скобка.
Ниже baseline — пары «путь|ключ», которые уже есть в проекте: на них детектор
молчит. Всё, что вне baseline, печатается предупреждением.
.PARAMETER ProjectRoot
Корень проекта. По умолчанию — текущий каталог.
.PARAMETER Baseline
Пары «путь|ключ» в нижнем регистре, которые считаются известными.
.PARAMETER SelfTest
Прогнать встроенные фикстуры и сверить результат с ожидаемым. Проект не читает.
.PARAMETER Quiet
Печатать только предупреждения (для вызова из другого скрипта).
.EXAMPLE
pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\Test-WriteGuardHazards.ps1 -SelfTest
.EXAMPLE
pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\Test-WriteGuardHazards.ps1 -ProjectRoot .
.OUTPUTS
Текст в stdout. Код возврата: 0 — чисто, 1 — есть строки вне baseline,
2 — ошибка самопроверки.
#>
[CmdletBinding()]
param(
[Parameter()][string] $ProjectRoot = (Get-Location).Path,
[Parameter()][string[]] $Baseline = @(),
[Parameter()][switch] $SelfTest,
[Parameter()][switch] $Quiet
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ------------------------------------------------------------------ правило
# Набор имён — из замеров, не из догадок. В служебных наборах продукта
# (журнал, память) список ШИРЕ; переносить его сюда нельзя: проверка записи
# не ловит passwd, private_key и credential.
$script:SensitiveNames = 'api[_-]?key|apikey|token|secret|password|authorization'
# Граница слова СЛЕВА отсутствует, а СПРАВА есть — это найдено самопроверкой
# и сверено с замером. Поэтому mytoken и DB_PASSWORD ловятся (подстрока),
# а token_value, tokens и password_hash — нет (после имени идёт словесный символ).
$script:HazardPattern = "($($script:SensitiveNames))\b[ \t]*[:=][ \t]*(\S+)"
$script:Extensions = @('.php', '.htm', '.html', '.md', '.sql', '.js', '.json', '.ps1',
'.css', '.py', '.yml', '.yaml', '.ini', '.conf', '.txt')
$script:SkipDirPattern = '(?:^|[\\/])(?:\.git|\.zwork|\.ragraf|\.agents|\.pi|vendor|node_modules|dist|build)(?:[\\/]|$)'
$script:RegexOptions = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
function Test-HazardLine {
<#
.SYNOPSIS
Возвращает имя найденного ключа или $null, если строка безопасна.
.DESCRIPTION
Отмена срабатывания: открывающая скобка в ПЕРВОМ ОТРЕЗКЕ значения.
Именно в первом, а не где угодно в строке: проверено замером.
#>
param([Parameter(Mandatory)][AllowEmptyString()][string] $Line)
$match = [regex]::Match($Line, $script:HazardPattern, $script:RegexOptions)
if (-not $match.Success) { return $null }
if ($match.Groups[2].Value.Contains('(')) { return $null }
return $match.Groups[1].Value.ToLowerInvariant()
}
# ------------------------------------------------------------- самопроверка
function Invoke-SelfTest {
<#
.SYNOPSIS
Фикстуры из замеров: ожидаемый результат против фактического.
.DESCRIPTION
Имена и знаки склеиваются из частей намеренно. Если записать проверяемую
пару целиком, файл этого скрипта сам попадёт под проверку записи, и
скрипт нельзя будет сохранить. Это не украшение, а условие
работоспособности.
#>
$eq = ' ' + '=' + ' '
$colon = ':' + ' '
$tok = 'tok' + 'en'
$pw = 'pass' + 'word'
$apk = 'api' + 'key'
$cases = @(
@{ Name = 'token'; Line = ($tok + $eq + 'v'); Blocked = $true }
@{ Name = 'password'; Line = ($pw + $eq + 'v'); Blocked = $true }
@{ Name = 'secret'; Line = ('sec' + 'ret' + $eq + 'v'); Blocked = $true }
@{ Name = 'authorization'; Line = ('authori' + 'zation' + $eq + 'v'); Blocked = $true }
@{ Name = 'apikey'; Line = ($apk + $eq + 'v'); Blocked = $true }
@{ Name = 'api_key'; Line = ('api' + '_key' + $eq + 'v'); Blocked = $true }
@{ Name = 'api-key'; Line = ('api' + '-key' + $eq + 'v'); Blocked = $true }
@{ Name = 'префикс слева'; Line = ('my' + $tok + $eq + 'v'); Blocked = $true }
@{ Name = 'CamelCase'; Line = ('session' + 'Token' + $eq + 'v'); Blocked = $true }
@{ Name = 'заглавные'; Line = (($tok).ToUpper() + $eq + 'v'); Blocked = $true }
@{ Name = 'двоеточие'; Line = ($tok + $colon + 'v'); Blocked = $true }
@{ Name = 'стрелка'; Line = ($tok + ' => ' + 'v'); Blocked = $true }
@{ Name = 'двойное равно'; Line = ($tok + ' == v'); Blocked = $true }
@{ Name = 'знак без пробела'; Line = ($tok + '=' + 'v'); Blocked = $true }
@{ Name = 'значение фигурное'; Line = ($tok + $eq + '{v}'); Blocked = $true }
@{ Name = 'закрывающая'; Line = ($tok + $eq + 'a)b'); Blocked = $true }
@{ Name = 'значение с точкой'; Line = ($tok + $eq + 'abc.def'); Blocked = $true }
@{ Name = 'скобка вне отрезка'; Line = ($tok + $eq + 'abc (v)'); Blocked = $true }
@{ Name = 'passwd'; Line = ('pass' + 'wd' + $eq + 'v'); Blocked = $false }
@{ Name = 'private_key'; Line = ('private' + '_key' + $eq + 'v'); Blocked = $false }
@{ Name = 'credential'; Line = ('credent' + 'ial' + $eq + 'v'); Blocked = $false }
@{ Name = 'cookie'; Line = ('coo' + 'kie' + $eq + 'v'); Blocked = $false }
@{ Name = 'ключ AWS'; Line = ('AWS_SECRET' + '_ACCESS_KEY' + $eq + 'v'); Blocked = $false }
@{ Name = 'имя с хвостом'; Line = ($tok + '_value' + $eq + 'v'); Blocked = $false }
@{ Name = 'имя с цифрой'; Line = ($pw + '2' + $eq + 'v'); Blocked = $false }
@{ Name = 'имя с пробелом'; Line = ('api' + ' key' + $eq + 'v'); Blocked = $false }
@{ Name = 'значение в скобках'; Line = ($tok + $eq + '(v)'); Blocked = $false }
@{ Name = 'скобка в отрезке'; Line = ($tok + $eq + 'abc(v)'); Blocked = $false }
@{ Name = 'только скобка'; Line = ($tok + $eq + '('); Blocked = $false }
@{ Name = 'знак без значения'; Line = ($tok + ' ='); Blocked = $false }
@{ Name = 'вовсе без знака'; Line = ($tok + ' v'); Blocked = $false }
@{ Name = 'восклицание'; Line = ($tok + ' != v'); Blocked = $false }
@{ Name = 'значение-строка'; Line = ('result' + $eq + 'v'); Blocked = $false }
)
$failed = 0
foreach ($case in $cases) {
$key = Test-HazardLine -Line $case.Line
$actual = $null -ne $key
if ($actual -ne $case.Blocked) {
$failed++
$expected = if ($case.Blocked) { 'БЛОК' } else { 'ПРОХОД' }
$got = if ($actual) { "БЛОК ($key)" } else { 'ПРОХОД' }
Write-Host (" [ОШИБКА] {0,-22} ожидалось {1,-7} получено {2}" -f $case.Name, $expected, $got) -ForegroundColor Red
}
}
Write-Host ("самопроверка: фикстур {0}, расхождений {1}" -f $cases.Count, $failed)
if ($failed -gt 0) {
Write-Host 'детектор расходится с замером — сверьте правило перед использованием' -ForegroundColor Red
return 2
}
Write-Host 'детектор совпадает с замером' -ForegroundColor Green
return 0
}
# -------------------------------------------------------------------- прогон
if ($SelfTest) { exit (Invoke-SelfTest) }
if (-not (Test-Path -LiteralPath $ProjectRoot -PathType Container)) {
Write-Host "каталог не найден: $ProjectRoot" -ForegroundColor Red
exit 1
}
$found = [System.Collections.Generic.List[string]]::new()
$fresh = [System.Collections.Generic.List[string]]::new()
$scanned = 0
foreach ($file in (Get-ChildItem -LiteralPath $ProjectRoot -Recurse -File -ErrorAction SilentlyContinue)) {
if ($script:Extensions -notcontains $file.Extension.ToLowerInvariant()) { continue }
if ($file.FullName -match $script:SkipDirPattern) { continue }
$scanned++
$relative = ([System.IO.Path]::GetRelativePath($ProjectRoot, $file.FullName)) -replace '\\', '/'
$lineNumber = 0
foreach ($line in [System.IO.File]::ReadAllLines($file.FullName)) {
$lineNumber++
$key = Test-HazardLine -Line $line
if ($null -eq $key) { continue }
$pair = "$relative|$key"
if (-not $found.Contains($pair)) { $found.Add($pair) }
if ($Baseline -contains $pair) { continue }
$fresh.Add("$relative`:$lineNumber — форма под защитой записи (ключ '$key')")
}
}
if (-not $Quiet) {
Write-Host "просмотрено файлов: $scanned"
Write-Host "найдено пар «путь и ключ»: $($found.Count)"
Write-Host "вне baseline: $($fresh.Count)"
Write-Host ''
Write-Host 'baseline для этого проекта (скопировать в параметр -Baseline):'
foreach ($pair in ($found | Sort-Object)) { Write-Host " '$pair'," }
}
foreach ($line in $fresh) { Write-Host " $line" -ForegroundColor Yellow }
if ($fresh.Count -gt 0) { exit 1 }
exit 0
B.3 Как этим пользоваться
Шаг 1. Проверить сам детектор. Убедиться, что он совпадает с замерами:
pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\Test-WriteGuardHazards.ps1 -SelfTest
Ожидаемый вывод: самопроверка: фикстур 35, расхождений 0 и код возврата 0.
Если есть расхождения — правило в скрипте правится под замер, а не наоборот.
Шаг 2. Первый прогон по проекту — получить список мест и готовый baseline:
pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\Test-WriteGuardHazards.ps1 -ProjectRoot .
В конце печатается готовый блок пар «путь|ключ» — его надо скопировать.
Шаг 3. Вписать baseline в свою проверку конвенций или передать параметром:
$baseline = @('auth.php|password', 'lib/html.php|token')
pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\Test-WriteGuardHazards.ps1 -ProjectRoot . -Baseline $baseline
Шаг 4. Регулярный прогон. Пусто — хорошо. Есть строки — переформулировать или осознанно внести в baseline.
Шаг 5. Убирать из baseline то, что починили. Иначе проверка будет «зелёной» на месте, которого уже нет: это ложная уверенность, а не экономия внимания.
B.4 Ограничения детектора
- Детектор повторяет модель правила, а не реализацию. Одна измеренная форма
(:=) расходится с моделью — см. A.8. На ней детектор даст предупреждение,
хотя реальная запись может пройти. - Только текст. Бинарные файлы, архивы, нетекстовые форматы не разбираются.
- Расширения — по списку. Файл с непривычным расширением не попадёт в обход.
- Никаких гарантий по версиям. Набор имён может измениться с обновлением
рантайма; детектор надо перепроверять на новых версиях. - Скобочная отмена упрощена. Детектор считает «первым отрезком» всё до
первого пробела; реальная проверка может разбирать сложнее — на многострочных
значениях возможны расхождения.
12. Приложение C. Системные инструкции, использующие этот скрипт
Ниже — тексты, которые уже внедрены и работают. Их можно скопировать в свой проект. Все три фрагмента ссылаются на скрипт из приложения B — это связка, благодаря которой правило живёт в инструкции, а проверка — в коде.
C.1 Общая инструкция агента (действует во всех проектах)
Этот блок добавлен в общий файл правил агента (~/.pi/agent/AGENTS.md, раздел 9).
Он даёт агенту понимание механизма, чтобы тот не лечил симптом.
## 9. Защита записи: как она работает и как с ней жить
Инструменты правки перед записью проверяют ВЕСЬ итоговый текст файла на форму
«учётное имя (token, secret, password, apikey, authorization), затем знак
равенства или двоеточие, затем значение». Одна такая строка блокирует запись
ФАЙЛА ЦЕЛИКОМ, даже если она чужая и лежит далеко от места правки.
Два разных механизма, которые нельзя путать:
1. Маскирование вывода — безвредно. В выводе инструментов видно [REDACTED];
файл на диске НЕ меняется (проверено сравнением байтов и контрольной суммы).
2. Отказ в записи — то, что мешает. Проверяется итоговый текст файла; сообщение
содержит номер строки и имя ключа.
Отключить проверку нельзя: это часть подписанного рантайма, а не настройка.
Патчить бинарник запрещено — это сломает установку и не переживёт обновления.
Что делать при отказе:
1. Прочитать сообщение: там номер строки и имя ключа — это точная диагностика.
2. Если это комментарий или документация — переформулировать: отделить имя от
значения СЛОВОМ, а не знаком («токен формы», «значение берётся из формы»).
3. Если это код — не подделывать содержимое. Правка, которая УДАЛЯЕТ блокирующую
строку, проходит; правка, которая её сохраняет, — нет.
4. Значение, если оно настоящее, читать из конфига или переменной окружения.
Чего делать нельзя: кодировать значение, разрезать строку, подменять символы,
обходить проверку другим инструментом письма ради маскировки.
Проверка «по значению» не работает: реальный ключ без учётного имени слева она
пропустит. Это проверка формы строки, а не защита от утечки в файл.
Перед сдачей результата прогонять детектор проекта:
pwsh -NoLogo -NoProfile -NonInteractive -File .\tools\Test-WriteGuardHazards.ps1 -ProjectRoot .
C.2 Инструкция проекта (правила проекта)
Этот блок добавлен в AGENTS.md проекта как ловушка — чтобы следующий агент не
проходил путь заново.
12. **Защита записи отказывает в правке файла из-за одной строки в нём.**
Инструмент правки проверяет ИТОГОВЫЙ текст файла на форму «учётное имя
(token, secret, password, apikey, authorization), затем знак равенства или
двоеточие, затем значение без пробела». Найденная строка блокирует запись
ЦЕЛИКОМ: не отдельный участок, а файл. Язык не разбирается, поэтому
легитимный код от секрета не отличается.
Замерено: блокируется сравнение переменной с пустой строкой, присваивание
из массива, флаг секрета в документации. НЕ блокируются: имя с хвостом
(password_hash), имена passwd, private_key, credential.
**Что делать.** Переформулировать строку: отделить имя от значения словом,
а не знаком (например: «токен формы», «значение берётся из формы»). Правка,
которая убирает такую строку, проходит; правка, которая её сохраняет, — нет.
Значение, если оно настоящее, читать из конфига или переменной окружения.
**Отключать проверку нельзя и не нужно:** переключателя не существует, а
патч подписанного рантайма не переживёт обновления.
Перечень известных мест на дереве записан в baseline проверки
`Test-WriteGuardHazards`; новые строки этой формы чекер печатает
предупреждением.
C.3 Проверка конвенций проекта (использует скрипт из B)
Блок для чекера конвенций проекта. Он не дублирует правило, а вызывает детектор: правило живёт в одном месте.
function Test-WriteGuardHazards {
<#
.SYNOPSIS
Строки, попадающие под защиту записи, — в пределах замера.
.DESCRIPTION
Правило восстановлено замерами (приложение A). Baseline — пары
«путь|ключ», уже найденные на дереве: на них проверка молчит, всё новое
печатается предупреждением.
Предупреждение, а не ошибка: такие строки есть в легитимном коде, и
объявлять их ошибкой — значит приучать игнорировать чекер.
#>
$problems = [System.Collections.Generic.List[string]]::new()
$examined = 0
$baseline = @(
# заполняется прогоном детектора: он печатает готовый блок
)
$detector = Join-Path $script:ProjectRoot 'tools/Test-WriteGuardHazards.ps1'
if (-not (Test-Path -LiteralPath $detector -PathType Leaf)) {
return New-CheckResult -Name 'защита записи: строки-ловушки в пределах замера' `
-Ok $false -Examined 0 `
-Details @('нет детектора tools/Test-WriteGuardHazards.ps1 — проверка невозможна')
}
$examined++
$output = & pwsh -NoLogo -NoProfile -NonInteractive -File $detector `
-ProjectRoot $script:ProjectRoot -Quiet 2>&1
foreach ($line in $output) {
$text = [string]$line
if ($text -notmatch 'форма под защитой записи') { continue }
# Пары из baseline детектор и сам не печатает (они уходят в -Baseline),
# поэтому здесь достаточно собрать всё, что он вывел.
$problems.Add($text.Trim())
}
return New-CheckResult -Name 'защита записи: строки-ловушки в пределах замера' `
-Ok ($problems.Count -eq 0) -Examined $examined -Details $problems `
-Warning:($problems.Count -gt 0)
}
Почему вызов внешним процессом, а не встроенный шаблон. Два места с одним правилом рано или поздно разойдутся: поправят скрипт — а чекер останется со старым набором имён. Именно так в версии 1.0 этой статьи появились три неверных утверждения. Правило должно быть одно.
Как передать baseline в детектор. Если нужно, чтобы детектор молчал на известных местах, передайте ему список при вызове:
$output = & pwsh -NoLogo -NoProfile -NonInteractive -File $detector `
-ProjectRoot $script:ProjectRoot -Baseline $baseline -Quiet 2>&1
Тогда в $output останутся только новые находки, и разбор в цикле упрощается.
C.4 Порядок внедрения в новом проекте
- Положить скрипт из приложения B в
tools/. - Прогнать самопроверку:
-SelfTest. Должно быть 0 расхождений. - Прогнать по проекту, получить список мест и baseline.
- Вставить блок C.3 в чекер конвенций, вписать baseline.
- Вставить блок C.2 в
AGENTS.mdпроекта. - Если это ваша общая среда — вставить блок C.1 в общий файл правил агента.
- Записать факт в отчёте: сколько мест, что решено переформулировать, что
осталось в baseline.
13. Приложение D. Методика замера (чтобы повторить)
Замер воспроизводим и не требует доступа к исходникам продукта.
- Готовим фикстуры. По одному правилу на файл. Ключи проверяемых строк
склеиваются из частей в генераторе — иначе генератор попадёт под проверку
и замер не состоится. - Применяем безобидную правку к каждому файлу: заменяем строку-маркер,
не содержащую учётных имён. - Читаем результат. Отказ
artifact_secret_detected— форма опасна.
Успешная правка — форма безопасна. - Сверяем с моделью. Если факт противоречит модели — правим модель,
а не факт. - Фиксируем условия: дата, версия рантайма, число фикстур, инструмент
правки (apply_patchилиEdit).
Практические оговорки:
- Проверка отравляет файлы. После серии надо удалять фикстуры: иначе они
попадут в отчёты чекеров, а часть из них нельзя будет править. - Инструмент правки и полная запись ведут себя по-разному. Для замера нужен
именно тот инструмент, которым вы работаете. - Не используйте настоящие значения. Все замеры — на плейсхолдерах; отчёт
печатает путь и имя ключа, но не содержимое. - Уже установленные формы не воспроизводите «чтобы проверить». Если форма
измерена, повторять её в рабочем файле незачем. - Своя проверка обязательна. Набор имён может измениться с обновлением
рантайма; замер имеет срок годности, а детектор — покажет факт.
14. Итог
artifact_secret_detected— проверка формы строки, а не вашего кода и не утечки.- Проверяется весь файл целиком; одна строка делает файл неправимым.
- Ловятся четыре слова-хвоста (
token,secret,password,authorization) и семействоapikey;passwd,private_key,credential— не ловятся. - Срабатывание отменяется, если в первом отрезке значения есть открывающая
круглая скобка.= (v)проходит,= abc (v)— нет. - Отключить нельзя — это часть рантайма, а не настройка. Патч не переживёт обновления.
- Чинится формулировкой: имя и значение разделяются словом.
- Правка, удаляющая строку, проходит — отравленный файл лечится штатно.
[REDACTED]в выводе — безвредное маскирование; файл на диске цел.- Ставьте детектор (приложение B) с baseline: он сам сверяется с замером
(-SelfTest) и находит новые ловушки до первой неудачной правки. - Настоящий секрет — в конфиг или переменную окружения, а не в текст программы.
Эта проверка от такой утечки не защищает: значение без учётного имени
слева она пропускает (замерено, A.8).
Приложения A–D добавлены в версии 2.0. Все результаты в таблицах — замеры 5 октября 2026 г. на реальной установке; способ повторения — в приложении D.