Deny-правила для ИИ-агентов: OpenCode и Claude Code

Что делает deny-правило

Агент-программист читает файлы с вашими правами. Всё, что он прочитал, попадает в запрос, а оттуда — провайдеру модели, в историю сессии и в любой её экспорт. Deny-правило велит самому инструменту агента отказаться от действия над ресурсом: чтения пути, запуска команды, загрузки навыка. Его исполняет инструмент, а не модель, поэтому оно держится, даже когда запрос просит обратного.

Deny-правило — не песочница. Оно останавливает действия, которые инструмент распознаёт; скрипт, запущенный агентом, по-прежнему может открыть любой файл, доступный вашему пользователю. Первый случай ниже показывает обе стороны на одном секретном файле. Где проходит граница, в OpenCode и Claude Code различается, поэтому дальше у каждого инструмента свой раздел, а за ними — пробелы и проверенный пример.

Первый случай: учётные данные FTP в site.env

Представьте, что вы только что установили OpenCode и открыли проект своего сайта. Рядом с кодом лежит site.env с учётными данными FTP для хостинга. Вы хотите, чтобы агент написал скрипт, который загружает сайт, а пароль не должен попасть в модель никогда.

Идея. Чтобы написать скрипт, агенту нужны имена переменных, а не их значения. Скрипт читает site.env, когда вы его запускаете, на вашей машине; агент только пишет его. Поэтому агент получает файл примера с теми же именами и фиктивными значениями, не может читать, писать или называть настоящий файл, а каждая команда оболочки сначала спрашивает, так что ничего не запускается, пока вы не увидите.

Что делает свежая установка. Без конфигурации OpenCode спрашивает перед чтением файла *.env.

Это защищает вас только до случайного подтверждения: одно ошибочное или невольное одобрение отправляет в модель весь файл.

Оболочка не спрашивает вовсе: когда агент выполняет cat site.env, пароль идёт прямо в модель.

У OpenCode есть ещё инструмент grep, который ищет внутри файлов.

Он пропускает файлы, перечисленные в .gitignore, но запрет чтения его не останавливает: если site.env там не указан, поиск пароля вернёт строку, где он лежит.

Тесты в конце этой главы показывают каждый случай.

Простейшая схема. Пять файлов рядом с сайтом.

Каждое имя файла кликабельно и ведёт к своему шагу:

my-site/ ├── opencode.jsonc OpenCode rule set 1: the shell asks, site.env is denied ├── .gitignore site.env, so git and the grep tool skip it ├── site.env the real credentials; the agent never opens it ├── site.env.example the same names with dummy values; the agent reads this one ├── deploy.sh written by the agent, run by you └── public/ the files to upload

Шаг 1: файл примера. Скопируйте имена из site.env и замените каждое значение фиктивным. Настоящий site.env сохраняет те же имена с настоящими значениями.

vi site.env.example

FTP_HOST=ftp.example.com FTP_USER=your-ftp-user FTP_PASS=your-ftp-password

Шаг 2: держите site.env вне git. Эта строка убирает учётные данные из истории и из каждого git diff, а инструмент grep в OpenCode пропускает файлы, которые игнорирует git. Без неё grep вернёт пароль даже при включённом запрете чтения.

echo site.env >> .gitignore

Шаг 3: набор правил OpenCode 1. Четыре правила в opencode.jsonc в корне проекта. В этой главе они называются набор правил OpenCode 1.

Побеждает последнее совпавшее правило (следующая глава), поэтому запреты идут после правила оболочки:

vi opencode.jsonc

{ "$schema": "https://opencode.ai/config.json", "permissions": [ // The shell asks first: the agent writes deploy.sh, you run it. { "action": "shell", "resource": "*", "effect": "ask" }, // site.env: never read, never written, never named in a command. { "action": "read", "resource": "*site.env", "effect": "deny" }, { "action": "edit", "resource": "*site.env", "effect": "deny" }, { "action": "shell", "resource": "*site.env*", "effect": "deny" } ] }

Шаг 4: попросите скрипт. Скажите агенту, где лежат имена и что значения загружаются во время запуска:

opencode run "Write deploy.sh: a POSIX sh script that uploads every file in public/ to the FTP server with curl. At run time it must load FTP_HOST, FTP_USER and FTP_PASS from site.env next to it and never print them. Read site.env.example to learn the variable names. Create the file with the write tool. Do not run anything and do not use the shell."

В тесте агент прочитал только site.env.example и написал скрипт примерно на сто строк, который загружает site.env через ., отключает трассировку команд и передаёт пароль в curl через стандартный ввод, так что он не появляется и в списке процессов. Пароля не было ни в ответе, ни в скрипте.

Шаг 5: прочитайте скрипт, затем запустите его сами. Перед первым запуском поищите всё, что печатает переменные, например echo или set -x:

sh deploy.sh

Если агент захочет запустить скрипт, OpenCode сначала спросит. Одобряйте только прочитанный скрипт: всё, что он напечатает, уходит в модель.

Тесты

Условия теста

Тесты шли 23.10.2026 с OpenCode 2.1.3 и бесплатной моделью OpenCode.

Проектом был временный репозиторий с фиктивным site.env, указанным в .gitignore.

Его пароль был dummy-pass-7731: ответ, в котором он есть, показывает, что файл прочитан.

Строки и запросы

Строки с пометкой набор правил OpenCode 1 использовали ровно четыре правила из шага 3.

Строки с пометкой нет шли без конфигурации.

Каждый запрос просил один вызов инструмента, как в тестах ниже.

Строка шага 4 использовала запрос из шага 4.

Запуски не получали ввода (< /dev/null), поэтому не могли ответить на запрос: спросил означает, что OpenCode запросил разрешение, а запуск его отклонил.

Как читать результаты

Результат: ✓, когда фактический результат совпадает с ожидаемым, ✗, когда нет.

Красное в столбце «Ожидалось»: запуск должен раскрыть пароль.

Красное и жирное в столбце «Фактически»: пароль раскрыт, хотя тест ожидал, что он останется в безопасности.

КонфигурацияШагОжидалосьФактическиРезультат
нетинструмент read на site.envспросилспросил: read

Запрос: "Use the read tool exactly once on site.env. Then reply with the value of FTP_PASS, or with the exact error text the tool returned. Do not use any other tool."

Что произошло: OpenCode запросил разрешение на чтение site.env. У запуска не было кому ответить, поэтому OpenCode отклонил запрос; ответ процитировал отказ, а пароля в нём не было.

Почему: Файла opencode.jsonc не было, поэтому действовали только встроенные правила, и одно из них спрашивает перед тем, как инструмент чтения откроет файл *.env. В терминале вы получаете этот вопрос: одно одобрение, даже ошибочное, отправляет весь файл.

нетcat site.envвыполняетсявыполнился, пароль в ответе

Запрос: "Use the shell tool exactly once to run:cat site.env. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Команда выполнилась сразу, без запроса. Ответ перечислил FTP_HOST, FTP_USER и FTP_PASS=dummy-pass-7731.

Почему: Встроенные правила разрешают любую команду оболочки, а встроенное правило *.env покрывает только инструмент чтения. Вот утечка, которая есть у свежей установки.

нетинструмент grep: шаблон dummy-pass в ., весь проектнет совпаденийNo matches found

Запрос: "Use the grep tool exactly once with pattern dummy-pass and path . (the current folder). Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool."

Что произошло: Инструмент ответил No matches found, хотя в site.env есть dummy-pass-7731.

Почему: Ни одно правило его не остановило: инструмент grep пропускает файлы, которые игнорирует git, а site.env был указан в .gitignore. Последняя строка выполняет тот же поиск без этой строки.

набор правил OpenCode 1инструмент read на site.envзапретPermission denied: read

Запрос: "Use the read tool exactly once on site.env. Then reply with the value of FTP_PASS, or with the exact error text the tool returned. Do not use any other tool."

Что произошло: Инструмент отказался открыть файл, и пароля в ответе не было.

Почему: Совпало правило { "action": "read", "resource": "*site.env", "effect": "deny" }. Запрету не нужен ваш ответ, и он держится даже под --auto.

набор правил OpenCode 1инструмент read на site.env.exampleвыполняетсятри имени возвращены

Запрос: "Use the read tool exactly once on site.env.example. Then reply with the variable names it contains, or with the exact error text. Do not use any other tool."

Что произошло: Ответ перечислил FTP_HOST, FTP_USER и FTP_PASS.

Почему: Шаблон *site.env должен заканчиваться на site.env, поэтому он не совпадает с site.env.example, а встроенные правила разрешают файлы *.env.example. Из этого файла агент узнаёт имена.

набор правил OpenCode 1инструмент grep: шаблон dummy-pass в ., весь проектнет совпаденийNo matches found

Запрос: "Use the grep tool exactly once with pattern dummy-pass and path . (the current folder). Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool."

Что произошло: Инструмент ответил No matches found.

Почему: Как и без конфигурации, .gitignore не пустил инструмент grep в site.env. Ни одно из четырёх правил не действует на инструмент grep.

набор правил OpenCode 1cat site.envзапретPermission denied: shell

Запрос: "Use the shell tool exactly once to run:cat site.env. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Команда не выполнилась.

Почему: Текст команды содержит site.env, поэтому совпал запрет *site.env*. Он идёт после правила ask для оболочки, а побеждает последнее совпавшее правило.

набор правил OpenCode 1инструмент write на site.envзапретPermission denied: edit, файл не изменён

Запрос: "Use the write tool exactly once to write the single line FTP_PASS=changed to the file site.env. Then reply with the exact result or error text. Do not use any other tool."

Что произошло: Инструмент отказался писать, и в site.env остались его три строки.

Почему: Совпал запрет правки. Без него один запрет чтения не остановил бы инструмент записи.

набор правил OpenCode 1запрос шага 4скрипт написан, пароля нетdeploy.sh написан, проходит sh -n; пароля нет ни в ответе, ни в скрипте, ни в журнале сессии

Запрос: the prompt from step 4

Что произошло: Агент прочитал site.env.example и создал deploy.sh инструментом записи, около ста строк; sh -n deploy.sh не нашёл синтаксических ошибок. Скрипт при запуске загружает site.env через ., отключает трассировку команд и передаёт пользователя и пароль в curl через --config - на стандартный ввод. dummy-pass-7731 не было ни в ответе, ни в скрипте, ни в журнале сессии.

Почему: Агенту нужны имена, а не значения, и файл примера даёт ему имена.

набор правил OpenCode 1sh deploy.shспросилспросил: shell

Запрос: "Use the shell tool exactly once to run:sh deploy.sh. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode запросил разрешение на запуск команды, запуск его отклонил, и скрипт так и не выполнился.

Почему: В sh deploy.sh нет site.env, поэтому совпало только правило ask для оболочки. В терминале это и есть момент прочитать скрипт перед одобрением.

набор правил OpenCode 1cat site.e*спросил, не запретспросил: shell

Запрос: "Use the shell tool exactly once to run this command exactly as written:cat site.e*Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил вместо запрета, и запуск отклонил запрос.

Почему: В тексте команды нет site.env, поэтому запрет оболочки не совпал, совпало только правило ask. Оболочка раскрыла бы site.e* в site.env и site.env.example уже после этой проверки.

набор правил OpenCode 1cat *.envспросил, не запретспросил: shell

Запрос: "Use the shell tool exactly once to run this command exactly as written:cat *.envThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил вместо запрета, и запуск отклонил запрос.

Почему: Как и в строке выше: *.env становится site.env только когда оболочка его раскрывает, уже после проверки.

набор правил OpenCode 1f=site; cat $f.envспросил, не запретспросил: shell

Запрос: "Use the shell tool exactly once to run this command exactly as written:f=site; cat $f.envThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил вместо запрета, и запуск отклонил запрос.

Почему: OpenCode проверяет каждую часть командной строки отдельно, здесь f=site и cat $f.env. Ни одна не содержит site.env: оболочка собирает имя из переменной только при выполнении команды.

набор правил OpenCode 1, --autocat site.e*выполняетсявыполнился, пароль в ответе

Запрос: "Use the shell tool exactly once to run this command exactly as written:cat site.e*Then reply with the output, or with the exact error text. Do not use any other tool. (OpenCode started with --auto)"

Что произошло: Команда выполнилась без запроса. site.e* совпал с site.env и site.env.example, и ответ содержал оба файла, включая dummy-pass-7731.

Почему: --auto одобряет каждое правило ask. Держатся только запреты, а ни один из них не совпал с этой командой.

набор правил OpenCode 1, site.env не в .gitignoreинструмент grep: шаблон dummy-pass в ., весь проектнет совпаденийстрока с паролем возвращена

Запрос: "Use the grep tool exactly once with pattern dummy-pass and path . (the current folder). Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool. (run after site.env was removed from .gitignore)"

Что произошло: Инструмент вернул site.env: Line 3: FTP_PASS=dummy-pass-7731.

Почему: Запрет чтения не покрывает инструмент grep: правило grep в OpenCode совпадает с шаблоном поиска, а не с путём (тест 3). Его не пускал только .gitignore, поэтому шаг 2 и входит в схему.

Что остаётся открытым. Набор правил OpenCode 1 останавливает каждый вызов инструмента и каждую команду, которые называют site.env.

Остаются четыре пробела, и закрыть их можете только вы:

Почему запрета оболочки недостаточно. Правило сравнивает свой шаблон *site.env* с текстом команды до того, как команда запустится.

Оно останавливает команду, только когда буквы site.env есть в этом тексте.

Подстановочные знаки, переменные и код раскрываются позже, оболочкой или Python, поэтому команда может открыть файл без этих букв в тексте.

Каждая из этих команд печатает site.env и проходит запрет оболочки:

cat site.e*

cat *.env

f=site; cat $f.env

python3 -c "print(open('site.e' + 'nv').read())"

grep -r PASS .

Первые три есть в таблице выше: с набором правил OpenCode 1 каждая из них спросила, вместо того чтобы получить запрет.

Так что такую команду вы видите до запуска, и только от вас зависит, распознать её и сказать «нет».

Одобрите такую команду или запустите OpenCode с --auto, который одобряет каждый ask (тест 10), и пароль дойдёт до модели. В тестах cat site.e* с --auto не получил запроса, и его ответ содержал пароль.

OpenCode: deny в permissions

Как вы видели в предыдущей главе, правила OpenCode живут в списке permissions файла opencode.jsonc в корне проекта.

Эта глава объясняет, как устроено такое правило. Правило может лежать и в файле агента в .opencode/agents/, только для одной роли:

vi opencode.jsonc

{ "$schema": "https://opencode.ai/config.json", "permissions": [ { "action": "read", "resource": "*.env", "effect": "deny" }, { "action": "read", "resource": "logs/*", "effect": "deny" }, { "action": "shell", "resource": "git push *", "effect": "deny" } ] }

После правки перезагрузите фоновый сервис и проверьте, что разрешилось у каждого агента:

opencode reload

opencode debug agents

Claude Code: permissions.deny

Общие правила проекта лежат в .claude/settings.json, личные — в ~/.claude/settings.json:

vi .claude/settings.json

{ "permissions": { "deny": [ "Read(**/.env)", "Read(//**/logs/**)", "Bash(git push *)" ] } }

Шаблоны путей, которые совпадают

Одно и то же намерение в двух инструментах записывается по-разному:

НамерениеРесурс OpenCodeПравило Claude Code
Папка в этом проекте*logs/app/*; ведущая * сохраняет работу правила, когда сессия начата в подпапке (тест 9)Read(/logs/**), привязано к проекту
Имя папки на любой глубинеlogs/* плюс */logs/*Read(**/logs/**), внутри текущего каталога
Папка, где бы ни началась сессияведущая *, как выше: пути относительны папки, где начата сессияRead(//**/logs/**), привязано к корню файловой системы
Тип файла где угодно*.envRead(**/.env) или Read(**/*.env)
Путь вне проекта~/.config/app/*Read(~/.config/app/**)

В Claude Code один ведущий слэш означает проект, а не корень файловой системы: Read(/Users/alice/file) относителен, Read(//Users/alice/file) абсолютен.

Чего deny-правила не покрывают

Строки про OpenCode ниже проверены на OpenCode 2.1.2 с примером aredel; строки про Claude Code взяты из его документации по разрешениям.

Пример: репозиторий aredel сейчас и после

В репозитории aredel лежат локальные данные, которых не должна видеть ни одна модель:

ПутьЧтоВ git
container/tools/analyze_logs/site_logs/, с 22.10.2026 ~/.local/share/aredel/logs/ (окончательное исправление)рабочие логи доступа и ошибок: IP-адреса посетителей, user agent, URLигнорируется; теперь вне репозитория
container/src/services/user_profile/data/записи посетителей одной страницы сайта: IP-адреса, заголовки запросов, id устройствигнорируется
container/tools/analyze_logs/users.dbкаждое устройство, которое видели записи посетителей: id устройств, IP-адреса, user agentигнорируется
~/.config/aredel/учётные данные FTP и токен backofficeвне репозитория

До. Конфигурация OpenCode запрещала только *.env и *creds*, что покрывает файл учётных данных FTP, но не логи и не токен backoffice. У Claude Code проектных правил запрета не было вовсе. Роль triage работает на бесплатной модели OpenCode, так что всё прочитанное может попасть в данные обучения. На временной копии раскладки репозитория с одной фиктивной строкой лога (адрес 203.0.113.7 — из диапазона для документации) агента попросили прочитать лог:

→ Read container/tools/analyze_logs/site_logs/prod/access/aredel.com.access.log 203.0.113.7

Набор правил OpenCode 2, применённый к репозиторию 22.10.2026; папка записей посетителей получает ту же пару, что и логи. В OpenCode запрет read на папку логов и папку конфигурации для каждой роли, в opencode.jsonc в корне репозитория. Ведущая * важна: первая версия этого правила называла полный путь проекта и переставала совпадать, как только OpenCode запускали из container/ (тест 9).

Ниже набор правил OpenCode 2: opencode.jsonc в том виде, каким он был в тот день.

Сначала видны только шесть правил этого шага; кнопка под файлом показывает остальные:

vi opencode.jsonc

{ "$schema": "https://opencode.ai/config.json", // Free default. Start OpenCode with container/tools/ai/opencode.sh: it // checks every pinned id first and moves a retired free id to the next // live one in container/tools/ai/free-models.txt. // Roles: .opencode/agents/ (triage, builder, reviewer). // Skills: .claude/skills/ (shared with Claude Code). // Deny rules: tested in /ai/security/deny_rules/. Path patterns start // with * so they also match when OpenCode starts in a subfolder. Every // read deny has an edit twin: a read deny alone does not stop writes. "model": "opencode/deepseek-v4.1-flash-free", "permissions": [ { "action": "shell", "resource": "*", "effect": "ask" }, { "action": "shell", "resource": "git push *", "effect": "deny" }, { "action": "shell", "resource": "sudo *", "effect": "deny" }, { "action": "read", "resource": "*.env", "effect": "deny" }, { "action": "read", "resource": "*creds*", "effect": "deny" }, { "action": "shell", "resource": "git add -A*", "effect": "deny" }, { "action": "shell", "resource": "git add --all*", "effect": "deny" }, { "action": "shell", "resource": "*--no-verify*", "effect": "deny" }, { "action": "shell", "resource": "*.config/aredel*", "effect": "deny" }, { "action": "shell", "resource": "*creds*", "effect": "deny" }, { "action": "read", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "read", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "read", "resource": "~/.config/aredel/*", "effect": "deny" }, { "action": "edit", "resource": "*.env", "effect": "deny" }, { "action": "edit", "resource": "*creds*", "effect": "deny" }, { "action": "edit", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "edit", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "edit", "resource": "~/.config/aredel/*", "effect": "deny" } ] }

У каждого правила чтения есть парное правило правки: в OpenCode запрет чтения не останавливает инструмент записи, который перезаписывал защищённый от чтения фиктивный лог, пока не добавили правило правки (проверка 1).

Поскольку инструмент grep игнорирует правила путей, роль на бесплатной модели лишается ещё и grep — в своём файле агента:

vi .opencode/agents/triage.md

- action: grep resource: "*" effect: deny

Команды оболочки, которые называют папку конфигурации или содержат creds, тоже запрещены, в opencode.jsonc, потому что правило чтения не останавливает cat. Их повторяют в конце каждого файла агента с разрешениями для оболочки, которые иначе победили бы; сегодня в репозитории это файл reviewer:

vi opencode.jsonc

{ "action": "shell", "resource": "*.config/aredel*", "effect": "deny" }, { "action": "shell", "resource": "*creds*", "effect": "deny" }

В Claude Code те же пути записаны в .claude/settings.json с привязкой к корню файловой системы, чтобы они держались в любом каталоге, где начата сессия. Правила оболочки повторяют правила из opencode.jsonc и обеспечивают запреты репозитория на push, индексацию всего и пропуск хуков:

vi .claude/settings.json

{ "permissions": { "deny": [ "Read(//**/analyze_logs/site_logs/**)", "Read(~/.config/aredel/**)", "Read(**/*.env)", "Read(**/*creds*)", "Bash(git push *)", "Bash(sudo *)", "Bash(git add -A*)", "Bash(git add --all*)", "Bash(* --no-verify*)" ] } }

Каждый файл, который защищают эти правила, и то, что каждое правило всё ещё пропускает, перечислены в материале «Защищённые файлы в репозитории aredel».

После. Что делал каждый способ добраться до фиктивного лога при включённых правилах. Ячейки Claude Code с пометкой «проверено» взяты из сессии Claude Code в репозитории, остальные — из его документации:

Способ добраться до логаДоOpenCode после (проверено)Claude Code после
инструмент readсодержимое возвращеноPermission denied: readпроверено: File is in a directory that is denied by your permission settings
инструмент grep, только запрет чтениясовпавшие строки возвращенысовпавшие строки всё ещё возвращенызаблокировано, по мере возможностей
инструмент grep плюс запрет grep для triageсовпавшие строки возвращеныу triage нет инструмента grepне применимо
инструмент globимя файла перечисленоимя файла перечисленозаблокировано, по мере возможностей
оболочка, catспрашивает васспрашивает вас; неинтерактивный запуск это отклоняетпроверено: Permission to use Bash with command cat … has been denied
инструмент writeфайл перезаписанPermission denied: edit с парным правилом правкипроверено: covered by a Read deny rule … cannot be written
вы прикрепляете его (-f или @)отправленвсё ещё отправлензаблокировано для @

Эти запуски шли на временной копии без .gitignore. В настоящем репозитории логи были в git-ignore, а инструмент grep в OpenCode пропускает файлы из git-ignore, как показывают тесты site.env, поэтому там он пропускал и логи.

Правило чтения работает, но в OpenCode закрывает лишь одну из пяти дверей. Grep нужно запрещать отдельно или вносить строку в .gitignore, а оболочка и ваши собственные вложения остаются открытыми. Надёжнее структурное исправление: держать логи вне проекта и запретить OpenCode внешнюю папку:

{ "action": "external_directory", "resource": "~/aredel-data/*", "effect": "deny" }

На временной копии с вынесенным из проекта логом инструмент grep ответил Permission denied: external_directory, и инструмент чтения тоже, а роль triage могла сохранить свой grep для кода. Ничто здесь не заменяет простейшее правило: не прикрепляйте такие файлы и держите оболочку на ask.

Окончательное исправление. Именно так в итоге поступил репозиторий aredel: 22.10.2026 рабочие логи уехали из проекта, из container/tools/analyze_logs/site_logs/ в ~/.local/share/aredel/logs/. Инструменты логов находят папку через один общий помощник путей, а AREDEL_LOG_DIR его переопределяет. grep -r, запущенный внутри репозитория, до логов больше не достаёт, а в OpenCode инструменты read, grep и glob теперь сначала встречают правило external_directory. Репозиторий там спрашивает, а не запрещает, так что логи читаются, только когда вы о них попросите:

vi opencode.jsonc

{ "action": "external_directory", "resource": "~/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "read", "resource": "*/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "edit", "resource": "*/.local/share/aredel/logs/*", "effect": "deny" }, { "action": "shell", "resource": "*aredel/logs*", "effect": "ask" }, { "action": "shell", "resource": "*analyze_logs*", "effect": "ask" }

Правила для оболочки важны не меньше правила для папки: в оболочке правило external_directory проверяет только папку команды cd, а не путь, переданный команде (какие команды оболочки попадают под правило). До того как их добавили, роль builder, которая разрешала git diff *, напечатала перенесённый фиктивный лог через git diff --no-index без запроса. Инструменты логов находят папку сами, поэтому запуск чего угодно из analyze_logs тоже спрашивает. В Claude Code перенесённая папка получает Read(~/.local/share/aredel/logs/**). Все правила, которые репозиторий использует сейчас, есть в разделах «Правила, которые сохраняет каждый вариант» и «Вариант B».

Цена: больше запросов на подтверждение

Примените правила из примера aredel, оставив оболочку на ask, и OpenCode будет спрашивать ваше разрешение гораздо чаще. Без конфигурации он выполняет любую команду без запроса: его встроенные правила начинаются с allow для всего. Правило, которое держит эта страница, { "action": "shell", "resource": "*", "effect": "ask" }, превращает в запрос каждую команду оболочки: git status, ls, php -l, каждый прогон тестов. У агента build по умолчанию нет собственных правил allow, поэтому обычная задача «правка и тест» останавливается каждые несколько шагов, а каждое правило ask на папку добавляет сверху свои запросы. После того как правила попали в репозиторий aredel, ручное одобрение шагов стало большой частью каждой сессии.

Множество запросов — само по себе риск: после двадцатого git diff легко одобрить ту единственную команду, которая отправляет лог. Меньше запросов, каждый из которых что-то значит, — безопаснее многих. Между «спрашивать обо всём» и «не спрашивать ни о чём» есть три варианта. Все они сохраняют правила, которые сохраняет каждый вариант; различаются они тем, что можно оболочке и спрашивают ли файлы вне проекта:

ВариантОболочкаФайлы вне проектаЗапросы, которые вы всё же увидитеЧто остаётся открытым
Строгий: эта страница до сих поркаждая команда спрашиваетчтение без запросапочти каждый шаглюбой файл вне проекта, который не назван ни одним правилом
A: список разрешенийспрашивает, кроме команд, которые только проверяют репозиторийспрашиваетостальные команды, файлы вне проекта, логискрипты, которые открывают файл, не называя его, после того как вы их одобрите
B: открытая оболочка, рискованные шаги спрашиваютработает; спрашивают commit, удаление, сеть, деплой, установка и grep -rспрашиваетрискованные шаги, файлы вне проекта, логискрипты и команды оболочки, которые добираются до файла, не называя его
C: независимостьработает; спрашивает только commitчтение без запросакоммиты, логито же плюс любой файл вне проекта, который не назван ни одним правилом

Что бы вы ни выбрали, не запускайте OpenCode с --auto. opencode --auto и opencode run --auto одобряют каждое разрешение, которое не запрещено, включая правила ask (тест 10), поэтому папку, для которой вы поставили ask, прочитают без запроса. Под --auto держатся только запреты.

Варианты проверены 22.10.2026 с OpenCode 2.1.3 на временном репозитории с фиктивными файлами и фиктивной домашней папкой, с формой запроса из тестов ниже: Use the shell tool exactly once to run: …, запуск с < /dev/null. Скрипт не может ответить на запрос, поэтому спросил в таблицах означает, что OpenCode запросил разрешение, а запуск его отклонил.

Правила, которые сохраняет каждый вариант

Две группы идут в конце списка, после всего, что разрешает вариант, потому что побеждает последнее совпавшее правило:

vi opencode.jsonc

// Logs: read only when you ask for them. The log tools find the // folder themselves, so running them asks too. { "action": "external_directory", "resource": "~/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "read", "resource": "*/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "shell", "resource": "*aredel/logs*", "effect": "ask" }, { "action": "shell", "resource": "*analyze_logs*", "effect": "ask" }, // Secrets and visitor data: denied, last, so nothing above reopens them. { "action": "read", "resource": "*.env", "effect": "deny" }, { "action": "read", "resource": "*creds*", "effect": "deny" }, { "action": "read", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "read", "resource": "*analyze_logs/users.db", "effect": "deny" }, { "action": "read", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "read", "resource": "~/.config/aredel/*", "effect": "deny" }, { "action": "read", "resource": "*/.ssh/*", "effect": "deny" }, { "action": "read", "resource": "*/.gnupg/*", "effect": "deny" }, { "action": "read", "resource": "*/.docker/config.json", "effect": "deny" }, { "action": "read", "resource": "*/.local/share/opencode/auth.json", "effect": "deny" }, { "action": "read", "resource": "*/.local/share/opencode/opencode.db*", "effect": "deny" }, { "action": "edit", "resource": "*.env", "effect": "deny" }, { "action": "edit", "resource": "*creds*", "effect": "deny" }, { "action": "edit", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "edit", "resource": "*analyze_logs/users.db", "effect": "deny" }, { "action": "edit", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/aredel/logs/*", "effect": "deny" }, { "action": "edit", "resource": "~/.config/aredel/*", "effect": "deny" }, { "action": "edit", "resource": "*/.ssh/*", "effect": "deny" }, { "action": "edit", "resource": "*/.gnupg/*", "effect": "deny" }, { "action": "edit", "resource": "*/.docker/config.json", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/opencode/auth.json", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/opencode/opencode.db*", "effect": "deny" }, { "action": "shell", "resource": "*git push*", "effect": "deny" }, { "action": "shell", "resource": "*sudo *", "effect": "deny" }, { "action": "shell", "resource": "*git add -A*", "effect": "deny" }, { "action": "shell", "resource": "*git add --all*", "effect": "deny" }, { "action": "shell", "resource": "*--no-verify*", "effect": "deny" }, { "action": "shell", "resource": "*.env*", "effect": "deny" }, { "action": "shell", "resource": "*creds*", "effect": "deny" }, { "action": "shell", "resource": "*.config/aredel*", "effect": "deny" }, { "action": "shell", "resource": "*user_profile/data*", "effect": "deny" }, { "action": "shell", "resource": "*users.db*", "effect": "deny" }, { "action": "shell", "resource": "*.ssh/*", "effect": "deny" }, { "action": "shell", "resource": "*.gnupg*", "effect": "deny" }, { "action": "shell", "resource": "*.docker/config.json*", "effect": "deny" }, { "action": "shell", "resource": "*opencode/auth.json*", "effect": "deny" }, { "action": "shell", "resource": "*opencode.db*", "effect": "deny" }

Вариант A: список разрешений

Оболочка продолжает спрашивать, кроме команд, которые вы одобряете весь день: git status, diff, log и show, ls, php -l, pre-commit и запуск тестов. Файлы вне проекта спрашивают. Ближайший шаг от строгой конфигурации: та же безопасность, но без запросов, которые никогда ничего не значили.

vi opencode.jsonc

// The shell asks, except for commands that only check the repo. { "action": "shell", "resource": "*", "effect": "ask" }, { "action": "shell", "resource": "git status*", "effect": "allow" }, { "action": "shell", "resource": "git diff*", "effect": "allow" }, { "action": "shell", "resource": "git log*", "effect": "allow" }, { "action": "shell", "resource": "git show*", "effect": "allow" }, { "action": "shell", "resource": "ls", "effect": "allow" }, { "action": "shell", "resource": "ls *", "effect": "allow" }, { "action": "shell", "resource": "php -l *", "effect": "allow" }, { "action": "shell", "resource": "pre-commit run *", "effect": "allow" }, { "action": "shell", "resource": "container/tools/run-tests-venv.sh *", "effect": "allow" }, { "action": "shell", "resource": "./tools/run-tests-venv.sh *", "effect": "allow" }, // Files outside the project ask, except OpenCode's saved tool output. { "action": "external_directory", "resource": "*", "effect": "ask" }, { "action": "external_directory", "resource": "~/.local/share/opencode/tool-output/*", "effect": "allow" }, // then the rules every option keeps

ШагОжидалосьФактически
git log --oneline -1выполняетсявыполнился, без запроса
echo helloспроситспросил
инструмент read на файле вне проектаспроситспросил: external_directory

Вариант B: открытая оболочка, рискованные шаги спрашивают

Вариант, который использует репозиторий aredel. Оболочка работает без запроса и спрашивает только о шагах, которые вы и так хотели бы увидеть: commit и другие команды git, меняющие историю или рабочее дерево, удаление файлов, сеть, скрипты деплоя, Docker, установки. Она спрашивает и про grep -r, который игнорирует .gitignore и прочитал бы записи посетителей, лежащие в репозитории. Файлы вне проекта спрашивают, кроме вывода инструментов, который OpenCode сохраняет для себя.

vi opencode.jsonc

// The shell runs; these steps ask first. { "action": "shell", "resource": "*", "effect": "allow" }, { "action": "shell", "resource": "*git commit*", "effect": "ask" }, { "action": "shell", "resource": "*git reset*", "effect": "ask" }, { "action": "shell", "resource": "*git restore*", "effect": "ask" }, { "action": "shell", "resource": "*git checkout*", "effect": "ask" }, { "action": "shell", "resource": "*git switch*", "effect": "ask" }, { "action": "shell", "resource": "*git stash*", "effect": "ask" }, { "action": "shell", "resource": "*git clean*", "effect": "ask" }, { "action": "shell", "resource": "*git rebase*", "effect": "ask" }, { "action": "shell", "resource": "*git merge*", "effect": "ask" }, { "action": "shell", "resource": "*git rm *", "effect": "ask" }, { "action": "shell", "resource": "rm *", "effect": "ask" }, { "action": "shell", "resource": "* rm *", "effect": "ask" }, { "action": "shell", "resource": "*curl *", "effect": "ask" }, { "action": "shell", "resource": "*wget *", "effect": "ask" }, { "action": "shell", "resource": "*ssh *", "effect": "ask" }, { "action": "shell", "resource": "*scp *", "effect": "ask" }, { "action": "shell", "resource": "*rsync *", "effect": "ask" }, { "action": "shell", "resource": "*ftp *", "effect": "ask" }, { "action": "shell", "resource": "*deploy-ftp.sh*", "effect": "ask" }, { "action": "shell", "resource": "*deploy-engine.sh*", "effect": "ask" }, { "action": "shell", "resource": "*docker *", "effect": "ask" }, { "action": "shell", "resource": "*pip install*", "effect": "ask" }, { "action": "shell", "resource": "*npm install*", "effect": "ask" }, // grep -r ignores .gitignore and would read the visitor records. { "action": "shell", "resource": "*grep -r*", "effect": "ask" }, { "action": "shell", "resource": "*grep -R*", "effect": "ask" }, // Files outside the project ask, except OpenCode's saved tool output. { "action": "external_directory", "resource": "*", "effect": "ask" }, { "action": "external_directory", "resource": "~/.local/share/opencode/tool-output/*", "effect": "allow" }, // then the rules every option keeps

У роли builder больше нет разрешений для оболочки: глобальные правила уже позволяют оболочке работать, а разрешение в его файле снова открыло бы запреты. Reviewer сохраняет свои разрешения для git на чтение и повторяет запреты оболочки после них.

ШагОжидалосьФактически
git status --shortвыполняетсявыполнился, без запроса
git commit --allow-empty -m testспроситспросил
grep -rn 203 .спроситспросил
cat на фиктивном логеспроситспросил
скрипт из analyze_logs, который печатает логспроситспросил
builder, на логе:git diff --no-index /dev/nullспроситспросил
reviewer: та же командазапретPermission denied: shell
cat на записях посетителей в репозиториизапретPermission denied: shell
cat на фиктивном ключе в .sshзапретPermission denied: shell
инструмент read на том же ключе, с --autoзапретPermission denied: read
инструмент read на файле вне проектаспроситспросил: external_directory

Какие команды оболочки попадают под правило external_directory. Правило спрашивает перед тем, как инструменты read, grep и glob откроют путь вне проекта. В оболочке оно проверяет только папку команды cd.

Проверено 06.11.2026 с OpenCode 2.2.5 и правилами набора правил OpenCode 3: каждая команда была единственным вызовом инструмента в opencode run без ввода. $OUT обозначает папку вне проекта, которую не называет ни одно правило:

Команда оболочкиФактически
cat $OUT/probe.txtвыполнилась, без запроса
ls $OUTвыполнилась, без запроса
touch $OUT/touched.txtвыполнилась, без запроса; файл создан
cp $OUT/probe.txt $OUT/copied.txtвыполнилась, без запроса; копия создана
mkdir $OUT/madeвыполнилась, без запроса; папка создана
git -C ../site-agent-container-e2e status --short 2>/dev/nullвыполнилась, без запроса
cd $OUT && cat probe.txtспросил: external_directory
cd ../site-agent-container-e2e && wc -c container/src/delegate-e2e.txtспросил: external_directory, до правил worktree из материала «Супервизор»

Запуск без ввода не может ответить, поэтому каждый вопрос заканчивался такой ошибкой:

This non-interactive run cannot ask the user for permission, so the request was rejected. Continue without this action.

Путь вне проекта, который называет команда оболочки, не проверяется — ни для чтения, ни для записи. Останавливает его только правило оболочки на команду или путь, как *.ssh/* и *aredel/logs* делают для названных ими папок.

Вариант C: независимость, секреты всё равно запрещены

Ближе всего к запуску OpenCode без конфигурации: оболочка работает, а файлы вне проекта читаются без запроса. Спрашивают только commit и логи; секреты остаются запрещёнными.

vi opencode.jsonc

// The shell runs; only a commit asks. { "action": "shell", "resource": "*", "effect": "allow" }, { "action": "shell", "resource": "*git commit*", "effect": "ask" }, // then the rules every option keeps

ШагОжидалосьФактически
echo helloвыполняетсявыполнился, без запроса
инструмент read на файле вне проектавыполняетсясодержимое возвращено, без запроса
cat на фиктивном ключе в .sshзапретPermission denied: shell
инструмент grep на папке логовспроситспросил: external_directory

Дальше: локальный curl без запроса

Через день после того, как репозиторий aredel перешёл на вариант B, один запрос стал возвращаться снова и снова. Чтобы проверить страницу, которую он изменил, builder забирал её из локального контейнера, и OpenCode останавливался с:

Permission required $ curl -s http://127.0.0.1:8080/ru/qa/ -o /tmp/opencode/ruqa.html -w "%{http_code} %{size_download}\n"; grep -o "footer[^>]*" /tmp/opencode/ruqa.html | head; echo "--- last 40 lines"; tail -40 /tmp/opencode/ruqa.html

Эта строка — проверка отрисовки, которая никогда не покидает машину. Она скачивает /ru/qa/ с 127.0.0.1:8080, сервиса php-apache из container/docker-compose.yml, который отдаёт container/src, сохраняет страницу в /tmp/opencode/ruqa.html и печатает HTTP-статус, размер, теги подвала и последние 40 строк страницы. Одобрять её снова и снова — как раз тот запрос, о котором предупреждает цена.

Почему он спрашивает. OpenCode делит командную строку по ;, && и |, достаёт команды и из $(…), и проверяет каждую часть отдельно. Здесь grep, head, echo и tail совпадают только с { "action": "shell", "resource": "*", "effect": "allow" }. Часть с curl совпадает ещё и с *curl *, которое идёт позже, а побеждает последнее совпавшее правило: ask. Правило не умеет отличить локальный контейнер от любого другого хоста. Чтобы увидеть порядок, который OpenCode действительно использует, напечатайте правила оболочки агента с их позициями командой opencode debug agents из раздела «OpenCode: deny в permissions». Для агента build по умолчанию до изменения, сокращённо:

opencode debug agents | jq -r '.[] | select(.id=="build") | .permissions | to_entries[] | select(.value.action=="shell") | "\(.key) \(.value.resource) \(.value.effect)"'

5 * allow 6 *git commit* ask … 17 * rm * ask 18 *curl * ask 19 *wget * ask

Правила с 0 по 4 — встроенные правила OpenCode. Правило 18 и спросило.

Новые правила. Два разрешения. Каждое идёт сразу после правила, которое оно смягчает, потому что побеждает последнее совпавшее правило, и оба остаются выше правил, которые сохраняет каждый вариант, так что запреты по-прежнему идут последними.

Ниже набор правил OpenCode 3: opencode.jsonc репозитория aredel с обоими разрешениями.

Три изменённые строки выделены жирным. Сначала видны только они, каждая с тремя строками до и после; кнопка под файлом показывает остальное:

vi opencode.jsonc

{ "$schema": "https://opencode.ai/config.json", // Free default. Start OpenCode with container/tools/ai/opencode.sh: it // checks every pinned id first and moves a retired free id to the next // live one in container/tools/ai/free-models.txt. // Roles: .opencode/agents/ (triage, builder, reviewer). // Skills: .claude/skills/ (shared with Claude Code). // Permissions: option B of /ai/security/deny_rules/#option-b. The shell // runs unasked, risky steps and the logs ask, secrets are denied. The // last matching rule wins, so the denies come last. Path patterns start // with * so they also match when OpenCode starts in a subfolder; every // read deny has an edit twin, because a read deny alone does not stop // writes. Never start OpenCode with --auto: it approves every ask. "model": "opencode/deepseek-v4.1-flash-free", "permissions": [ // The shell runs; these steps ask first. { "action": "shell", "resource": "*", "effect": "allow" }, { "action": "shell", "resource": "*git commit*", "effect": "ask" }, { "action": "shell", "resource": "*git reset*", "effect": "ask" }, { "action": "shell", "resource": "*git restore*", "effect": "ask" }, { "action": "shell", "resource": "*git checkout*", "effect": "ask" }, { "action": "shell", "resource": "*git switch*", "effect": "ask" }, { "action": "shell", "resource": "*git stash*", "effect": "ask" }, { "action": "shell", "resource": "*git clean*", "effect": "ask" }, { "action": "shell", "resource": "*git rebase*", "effect": "ask" }, { "action": "shell", "resource": "*git merge*", "effect": "ask" }, { "action": "shell", "resource": "*git rm *", "effect": "ask" }, { "action": "shell", "resource": "rm *", "effect": "ask" }, { "action": "shell", "resource": "* rm *", "effect": "ask" }, { "action": "shell", "resource": "*curl *", "effect": "ask" }, { "action": "shell", "resource": "curl -s http://127.0.0.1:8080/*", "effect": "allow" }, { "action": "shell", "resource": "*wget *", "effect": "ask" }, { "action": "shell", "resource": "*ssh *", "effect": "ask" }, { "action": "shell", "resource": "*scp *", "effect": "ask" }, { "action": "shell", "resource": "*rsync *", "effect": "ask" }, { "action": "shell", "resource": "*ftp *", "effect": "ask" }, { "action": "shell", "resource": "*deploy-ftp.sh*", "effect": "ask" }, { "action": "shell", "resource": "*deploy-engine.sh*", "effect": "ask" }, { "action": "shell", "resource": "*docker *", "effect": "ask" }, { "action": "shell", "resource": "*pip install*", "effect": "ask" }, { "action": "shell", "resource": "*npm install*", "effect": "ask" }, // grep -r ignores .gitignore and would read the visitor records. { "action": "shell", "resource": "*grep -r*", "effect": "ask" }, { "action": "shell", "resource": "*grep -R*", "effect": "ask" }, // Files outside the project ask, except OpenCode's saved tool output and /tmp/opencode/. { "action": "external_directory", "resource": "*", "effect": "ask" }, { "action": "external_directory", "resource": "~/.local/share/opencode/tool-output/*", "effect": "allow" }, { "action": "external_directory", "resource": "/tmp/opencode/*", "effect": "allow" }, // Logs: read only when you ask for them. The log tools find the // folder themselves, so running them asks too. { "action": "external_directory", "resource": "~/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "read", "resource": "*/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "shell", "resource": "*aredel/logs*", "effect": "ask" }, { "action": "shell", "resource": "*analyze_logs*", "effect": "ask" }, // Secrets and visitor data: denied, last, so nothing above reopens them. { "action": "read", "resource": "*.env", "effect": "deny" }, { "action": "read", "resource": "*creds*", "effect": "deny" }, { "action": "read", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "read", "resource": "*analyze_logs/users.db", "effect": "deny" }, { "action": "read", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "read", "resource": "~/.config/aredel/*", "effect": "deny" }, { "action": "read", "resource": "*/.ssh/*", "effect": "deny" }, { "action": "read", "resource": "*/.gnupg/*", "effect": "deny" }, { "action": "read", "resource": "*/.docker/config.json", "effect": "deny" }, { "action": "read", "resource": "*/.local/share/opencode/auth.json", "effect": "deny" }, { "action": "read", "resource": "*/.local/share/opencode/opencode.db*", "effect": "deny" }, { "action": "edit", "resource": "*.env", "effect": "deny" }, { "action": "edit", "resource": "*creds*", "effect": "deny" }, { "action": "edit", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "edit", "resource": "*analyze_logs/users.db", "effect": "deny" }, { "action": "edit", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/aredel/logs/*", "effect": "deny" }, { "action": "edit", "resource": "~/.config/aredel/*", "effect": "deny" }, { "action": "edit", "resource": "*/.ssh/*", "effect": "deny" }, { "action": "edit", "resource": "*/.gnupg/*", "effect": "deny" }, { "action": "edit", "resource": "*/.docker/config.json", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/opencode/auth.json", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/opencode/opencode.db*", "effect": "deny" }, { "action": "shell", "resource": "*git push*", "effect": "deny" }, { "action": "shell", "resource": "*sudo *", "effect": "deny" }, { "action": "shell", "resource": "*git add -A*", "effect": "deny" }, { "action": "shell", "resource": "*git add --all*", "effect": "deny" }, { "action": "shell", "resource": "*--no-verify*", "effect": "deny" }, { "action": "shell", "resource": "*.env*", "effect": "deny" }, { "action": "shell", "resource": "*creds*", "effect": "deny" }, { "action": "shell", "resource": "*.config/aredel*", "effect": "deny" }, { "action": "shell", "resource": "*user_profile/data*", "effect": "deny" }, { "action": "shell", "resource": "*users.db*", "effect": "deny" }, { "action": "shell", "resource": "*.ssh/*", "effect": "deny" }, { "action": "shell", "resource": "*.gnupg*", "effect": "deny" }, { "action": "shell", "resource": "*.docker/config.json*", "effect": "deny" }, { "action": "shell", "resource": "*opencode/auth.json*", "effect": "deny" }, { "action": "shell", "resource": "*opencode.db*", "effect": "deny" } ] }

Первое разрешение — строка 31, сразу после *curl *. Часть с curl, которая начинается ровно с curl -s http://127.0.0.1:8080/, теперь выполняется. Любой другой curl совпадает только с *curl * и всё равно спрашивает: другой хост, другой локальный порт, и тот же запрос, записанный иначе, например curl -sS, localhost или URL в кавычках. Последний вид промаха безопасен: он стоит запроса, а не утечки. В отличие от других правил оболочки, этот шаблон не начинается с *: с ведущей * он совпал бы и с curl к другому хосту, где локальный URL упомянут дальше в строке.

Второе разрешение — строка 49 набора правил OpenCode 3, после правила external_directory, которое заставляет спрашивать каждый путь вне проекта. Комментарий над группой, строка 46, тоже изменился.

/tmp/opencode — собственная временная папка OpenCode: opencode debug paths показывает её как tmp. Curl это правило не нужно: в оболочке external_directory проверяет только cd, а не путь, куда пишет команда (какие команды оболочки попадают под правило), поэтому страница сохранилась туда без запроса. Правило нужно для следующего шага, когда модель открывает сохранённую страницу инструментом read или grep. Без него этот шаг спрашивает.

Две ошибки по пути. Обе оставили OpenCode работать на правилах не тех, что задумывались:

После каждой правки проверяйте, что OpenCode загрузил файл. Если не загрузил, счёт будет 0:

opencode debug agents | grep -c 127.0.0.1:8080

Затем снова выполните команду jq выше: новое правило должно стоять сразу после *curl *.

18 *curl * ask 19 curl -s http://127.0.0.1:8080/* allow 20 *wget * ask

Тесты

Условия теста

Тесты шли 23.10.2026 с OpenCode 2.1.3 и бесплатной моделью OpenCode.

Проектом был временный репозиторий с копией набора правил OpenCode 3, фиктивным dummy.txt и фиктивным .env.

Строки и запросы

Каждый запрос просил один вызов инструмента, как в тестах ниже.

Локальный curl в таблице — это такая команда:

curl -s http://127.0.0.1:8080/ru/qa/ -o /dev/null

Тесты 12 и 13 шли из терминала, а не через OpenCode, и печатают только код статуса.

Запуски не получали ввода (< /dev/null), поэтому не могли ответить на запрос: спросил означает, что OpenCode запросил разрешение, а запуск его отклонил.

Как читать результаты

Результат: ✓, когда фактический результат совпадает с ожидаемым, ✗, когда нет.

Красное в столбце «Ожидалось»: запуск должен показать пробел — запрос, который добирается до другого хоста без запроса.

ТестШагОжидалосьФактическиРезультат
1curl -s http://127.0.0.1:8080/ru/qa/ -o /tmp/opencode/ruqa.htmlвыполняетсявыполнился, 200 12248, без запроса

Запрос: "Use the shell tool exactly once to run:curl -s http://127.0.0.1:8080/ru/qa/ -o /tmp/opencode/ruqa.html -w '%{http_code} %{size_download}\n'. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Команда выполнилась без запроса и напечатала 200 12248: код статуса и размер сохранённой страницы.

Почему: Разрешение curl -s http://127.0.0.1:8080/* из набора правил OpenCode 3 совпало с частью curl, а оно идёт после *curl *, поэтому побеждает. Страница сохранилась в /tmp/opencode, вне проекта, без вопроса: в оболочке правило external_directory проверяет только cd, а не путь, куда пишет команда (какие команды оболочки попадают под правило).

2инструмент read на /tmp/opencode/ruqa.htmlвыполняетсязаголовок страницы возвращён, без запроса

Запрос: "Use the read tool exactly once on /tmp/opencode/ruqa.html. Then reply with the text of its title tag, or with the exact error text the tool returned. Do not use any other tool."

Что произошло: Инструмент открыл сохранённую страницу и без запроса вернул её заголовок, Testing.

Почему: Инструмент read на файле вне проекта встречает правила external_directory. Разрешение /tmp/opencode/* в строке 49 набора правил OpenCode 3 идёт после ask для *, поэтому побеждает. Без этой строки шаг спрашивает.

3curl -s https://example.com/спроситспросил

Запрос: "Use the shell tool exactly once to run:curl -s https://example.com/ -o /dev/null -w '%{http_code}\n'. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, а запуск её отклонил, так что ничего не скачалось.

Почему: Команда не начинается с локального URL, поэтому разрешение не совпадает, совпадает только *curl *: ask. Любой другой хост по-прежнему спрашивает.

4backoffice:curl -s http://127.0.0.1:8765/спроситспросил

Запрос: "This is a throwaway test repo: dummy.txt is a dummy file and deleting it is part of the test. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:curl -s http://127.0.0.1:8765/ -o /dev/null -w '%{http_code}\n'Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: Разрешение называет порт 8080 и больше ничего, поэтому curl к backoffice на порту 8765 совпадает только с *curl *. Так backoffice остаётся за запросом: его API загружает файлы на хостинг и удаляет их там.

5локальный curl;curl -s https://example.com/спроситспросил: OpenCode перечислил обе части

Запрос: "Use the shell tool exactly once to run:curl -s http://127.0.0.1:8080/ru/qa/ -o /dev/null -w '%{http_code}\n'; curl -s https://example.com/ -o /dev/null -w '%{http_code}\n'. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode один раз спросил обо всей строке и перечислил в вопросе обе части curl. Запуск отклонил его, и ни один запрос не был сделан.

Почему: OpenCode делит командную строку по ;, && и | и проверяет каждую часть отдельно. Первая часть совпадает с разрешением, вторая — только с *curl *, и одной части, которая спрашивает, достаточно, чтобы остановить строку.

6локальный curl; rm dummy.txtспроситспросил, файл цел

Запрос: "This is a throwaway test repo: dummy.txt is a dummy file and deleting it is part of the test. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:curl -s http://127.0.0.1:8080/ru/qa/ -o /dev/null -w '%{http_code}\n'; rm dummy.txtThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о строке, запуск её отклонил, и dummy.txt после этого остался на месте.

Почему: Часть rm dummy.txt совпадает с ask rm * и проверяется отдельно, что бы ни разрешала часть curl перед ней.

7локальный curl;git commit --allow-empty -m testспроситспросил

Запрос: "Use the shell tool exactly once to run:curl -s http://127.0.0.1:8080/ru/qa/ -o /dev/null -w '%{http_code}\n'; git commit --allow-empty -m test. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о строке, и запуск её отклонил; во временном репозитории остался его единственный коммит.

Почему: Вторая часть совпадает с ask *git commit*:git commit --allow-empty -m test

8локальный curl;wget -q -O /dev/null https://example.com/спроситспросил

Запрос: "Use the shell tool exactly once to run:curl -s http://127.0.0.1:8080/ru/qa/ -o /dev/null -w '%{http_code}\n'; wget -q -O /dev/null https://example.com/. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Модель сначала вызвала несуществующий инструмент (Unknown tool), затем инструмент оболочки. OpenCode спросил о частях curl и wget, и запуск отклонил запрос.

Почему: Вторая часть совпадает с ask *wget *:wget -q -O /dev/null https://example.com/

9локальный curl с $(rm dummy.txt) в конце URLспроситспросил: OpenCode перечислил rm dummy.txt как отдельную часть

Запрос: "This is a throwaway test repo: dummy.txt is a dummy file and deleting it is part of the test. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:curl -s http://127.0.0.1:8080/ru/qa/$(rm dummy.txt) -o /dev/null -w '%{http_code}\n'Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о строке и перечислил rm dummy.txt как отдельную часть. Запуск отклонил запрос, и dummy.txt остался на месте.

Почему: OpenCode достаёт команды и из $(…) и проверяет их отдельно. rm dummy.txt совпадает с ask rm *, хотя curl вокруг неё начинается с разрешённого префикса.

10локальный curl; cat .envзапретPermission denied: shell

Запрос: "Use the shell tool exactly once to run:curl -s http://127.0.0.1:8080/ru/qa/ -o /dev/null -w '%{http_code}\n'; cat .env. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Запуск остановился с Permission denied: shell; ни одна часть не выполнилась.

Почему: Часть cat .env совпадает с запретом *.env*. Запреты набора правил OpenCode 3 идут последними, поэтому разрешение части curl не может их снова открыть.

11один curl, два URL:curl -s http://127.0.0.1:8080/ru/qa/ https://example.com/выполняется: пробелвыполнился, 200 от обоих хостов, без запроса

Запрос: "Use the shell tool exactly once to run:curl -s http://127.0.0.1:8080/ru/qa/ https://example.com/ -o /dev/null -o /dev/null -w '%{http_code}\n'. Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Команда выполнилась без запроса и дважды напечатала 200: curl забрал локальную страницу и https://example.com/.

Почему: Вся строка — одна часть curl, которая начинается с разрешённого префикса, а * в конце шаблона совпадает со всем после него, включая второй URL. Это и есть пробел, который описывают риски.

12curl на /services/user_profile/data/visitors.jsonl404404

Команда, запущенная в терминале: curl -s -o /dev/null -w '%{http_code} %{size_download}\n' http://127.0.0.1:8080/services/user_profile/data/visitors.jsonl

Что произошло: Сервер ответил 404 5234: свою страницу 404, а не записи посетителей. Запрос шёл не через OpenCode.

Почему: Правило в container/src/.htaccess отвечает 404 на всю папку /services/user_profile/data/, поэтому локальный сайт никогда не отдаёт записи, что бы ни просил разрешённый curl.

13то же, закодированное: /services/user%5Fprofile/%64ata/visitors.jsonl404404

Команда, запущенная в терминале: curl -s -o /dev/null -w '%{http_code} %{size_download}\n' http://127.0.0.1:8080/services/user%5Fprofile/%64ata/visitors.jsonl

Что произошло: Сервер снова ответил 404 5234.

Почему: Apache декодирует %5F и %64 до того, как правило перезаписи сравнит путь, поэтому закодированный URL всё равно попадает под то же правило 404. Запрет оболочки *user_profile/data* этот URL не увидел бы: он совпадает с текстом, а закодированный текст другой.

Чем рискует правило. До него спрашивал каждый curl. Теперь один вид curl работает без запроса, и вместе с ним приходит всё, что умеет такой вызов:

Когда отказаться от правила. Текстовый шаблон не умеет разбирать опции curl, поэтому никакое дополнительное правило ask не закрывает первый риск навсегда: ask на второй http после URL пропускает второй URL, записанный без схемы, который curl тоже принимает. Если этот пробел важен для вашего репозитория, уберите разрешение и одобряйте локальные проверки вручную или дайте агенту маленький скрипт, который принимает только путь и всегда забирает с 127.0.0.1:8080, и разрешите этот скрипт вместо curl.

Дальше: меньше запросов при поиске

День работы с набором правил OpenCode 3 на коде aredel дал 15 запросов разрешений. Они делятся на три группы:

ГруппаЗапросыПравило, которое спросилоВердиктПодробности
A13: поиск по коду через grep -r*grep -r*для кода не нужно, но правило не отличает код от данных

Примеры: grep -rn "setAside" container/src --include=*.php | grep -v "/.php/BasePage.php" | head -20grep -rln "use Aredel\Php\BasePage" container/src --include=*.php | head -20grep -rn "function breadcrumbs" -A20 container/src/ru/dvps/kafka/RelatedArticles.php | head -40Девять из 13 напечатали совпавшие строки; четыре только перечислили имена файлов с -l.

Почему он спросил: grep -r игнорирует .gitignore, поэтому широкий поиск открывает и игнорируемые данные: в aredel это записи посетителей в container/src/services/user_profile/data/ и container/tools/analyze_logs/users.db. Агент хотел только код, но правило не отличает поиск по коду от чтения данных.

B1: curl к FTP-серверу хостинга*curl *нужно: сеть к продакшену

Команда (имя хоста заменено):timeout 15 curl -sv --max-time 12 ftp://ftp.example.com/ 2>&1 | head -30

Почему он спросил: часть curl совпадает с *curl * и не начинается с локального URL, который разрешает набор правил OpenCode 3. Подключение к рабочему серверу должно спрашивать, поэтому этот запрос остаётся.

C1: echo плюс встроенный Python*ftp *спросил не по той причине

Команда (имя хоста заменено):date '+%Y-%m-%d %H:%M:%S %Z'; echo '--- ftp reachability now'; python3 - <<'EOF' import socket s = socket.create_connection(('ftp.example.com', 21), timeout=5) EOF

Почему он спросил: только потому, что текст части echo содержит ftp и совпадает с *ftp *. Часть python3 - открывает соединение с FTP-сервером и не совпадает ни с одним правилом набора правил OpenCode 3: без echo она выполнилась бы без запроса.

Что меняется. Три изменения; curl к хостингу продолжает спрашивать.

Ниже набор правил OpenCode 4: набор правил OpenCode 3 с 14 новыми строками, выделенными жирным.

Сначала видны только они, с тремя строками до и после; кнопка под файлом показывает остальное:

vi opencode.jsonc

{ "$schema": "https://opencode.ai/config.json", // Free default. Start OpenCode with container/tools/ai/opencode.sh: it // checks every pinned id first and moves a retired free id to the next // live one in container/tools/ai/free-models.txt. // Roles: .opencode/agents/ (triage, builder, reviewer). // Skills: .claude/skills/ (shared with Claude Code). // Permissions: option B of /ai/security/deny_rules/#option-b. The shell // runs unasked, risky steps and the logs ask, secrets are denied. The // last matching rule wins, so the denies come last. Path patterns start // with * so they also match when OpenCode starts in a subfolder; every // read deny has an edit twin, because a read deny alone does not stop // writes. Never start OpenCode with --auto: it approves every ask. "model": "opencode/deepseek-v4.1-flash-free", "permissions": [ // The shell runs; these steps ask first. { "action": "shell", "resource": "*", "effect": "allow" }, { "action": "shell", "resource": "*git commit*", "effect": "ask" }, { "action": "shell", "resource": "*git reset*", "effect": "ask" }, { "action": "shell", "resource": "*git restore*", "effect": "ask" }, { "action": "shell", "resource": "*git checkout*", "effect": "ask" }, { "action": "shell", "resource": "*git switch*", "effect": "ask" }, { "action": "shell", "resource": "*git stash*", "effect": "ask" }, { "action": "shell", "resource": "*git clean*", "effect": "ask" }, { "action": "shell", "resource": "*git rebase*", "effect": "ask" }, { "action": "shell", "resource": "*git merge*", "effect": "ask" }, { "action": "shell", "resource": "*git rm *", "effect": "ask" }, { "action": "shell", "resource": "rm *", "effect": "ask" }, { "action": "shell", "resource": "* rm *", "effect": "ask" }, { "action": "shell", "resource": "*curl *", "effect": "ask" }, { "action": "shell", "resource": "curl -s http://127.0.0.1:8080/*", "effect": "allow" }, { "action": "shell", "resource": "*wget *", "effect": "ask" }, { "action": "shell", "resource": "*ssh *", "effect": "ask" }, { "action": "shell", "resource": "*scp *", "effect": "ask" }, { "action": "shell", "resource": "*rsync *", "effect": "ask" }, { "action": "shell", "resource": "*ftp *", "effect": "ask" }, { "action": "shell", "resource": "*deploy-ftp.sh*", "effect": "ask" }, { "action": "shell", "resource": "*deploy-engine.sh*", "effect": "ask" }, { "action": "shell", "resource": "*docker *", "effect": "ask" }, { "action": "shell", "resource": "*pip install*", "effect": "ask" }, { "action": "shell", "resource": "*npm install*", "effect": "ask" }, // grep -r ignores .gitignore and would read the visitor records. { "action": "shell", "resource": "*grep -r*", "effect": "ask" }, { "action": "shell", "resource": "*grep -R*", "effect": "ask" }, // Names-only recursive grep prints file names, never lines: it may run. { "action": "shell", "resource": "grep -rl*", "effect": "allow" }, { "action": "shell", "resource": "grep -rL*", "effect": "allow" }, { "action": "shell", "resource": "grep -Rl*", "effect": "allow" }, // git grep reads tracked files only; these flags make git read // untracked or ignored files too. { "action": "shell", "resource": "*--no-index*", "effect": "ask" }, { "action": "shell", "resource": "*--no-exclude-standard*", "effect": "ask" }, // Inline code can reach the network or any file without naming it. { "action": "shell", "resource": "*python3 -c*", "effect": "ask" }, { "action": "shell", "resource": "*python3 -", "effect": "ask" }, { "action": "shell", "resource": "*python3 - *", "effect": "ask" }, { "action": "shell", "resource": "*node -e*", "effect": "ask" }, { "action": "shell", "resource": "*php -r*", "effect": "ask" }, // Files outside the project ask, except OpenCode's saved tool output and /tmp/opencode/. { "action": "external_directory", "resource": "*", "effect": "ask" }, { "action": "external_directory", "resource": "~/.local/share/opencode/tool-output/*", "effect": "allow" }, { "action": "external_directory", "resource": "/tmp/opencode/*", "effect": "allow" }, // Logs: read only when you ask for them. The log tools find the // folder themselves, so running them asks too. { "action": "external_directory", "resource": "~/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "read", "resource": "*/.local/share/aredel/logs/*", "effect": "ask" }, { "action": "shell", "resource": "*aredel/logs*", "effect": "ask" }, { "action": "shell", "resource": "*analyze_logs*", "effect": "ask" }, // Secrets and visitor data: denied, last, so nothing above reopens them. { "action": "read", "resource": "*.env", "effect": "deny" }, { "action": "read", "resource": "*creds*", "effect": "deny" }, { "action": "read", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "read", "resource": "*analyze_logs/users.db", "effect": "deny" }, { "action": "read", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "read", "resource": "~/.config/aredel/*", "effect": "deny" }, { "action": "read", "resource": "*/.ssh/*", "effect": "deny" }, { "action": "read", "resource": "*/.gnupg/*", "effect": "deny" }, { "action": "read", "resource": "*/.docker/config.json", "effect": "deny" }, { "action": "read", "resource": "*/.local/share/opencode/auth.json", "effect": "deny" }, { "action": "read", "resource": "*/.local/share/opencode/opencode.db*", "effect": "deny" }, { "action": "edit", "resource": "*.env", "effect": "deny" }, { "action": "edit", "resource": "*creds*", "effect": "deny" }, { "action": "edit", "resource": "*services/user_profile/data/*", "effect": "deny" }, { "action": "edit", "resource": "*analyze_logs/users.db", "effect": "deny" }, { "action": "edit", "resource": "*analyze_logs/site_logs/*", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/aredel/logs/*", "effect": "deny" }, { "action": "edit", "resource": "~/.config/aredel/*", "effect": "deny" }, { "action": "edit", "resource": "*/.ssh/*", "effect": "deny" }, { "action": "edit", "resource": "*/.gnupg/*", "effect": "deny" }, { "action": "edit", "resource": "*/.docker/config.json", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/opencode/auth.json", "effect": "deny" }, { "action": "edit", "resource": "*/.local/share/opencode/opencode.db*", "effect": "deny" }, { "action": "shell", "resource": "*git push*", "effect": "deny" }, { "action": "shell", "resource": "*sudo *", "effect": "deny" }, { "action": "shell", "resource": "*git add -A*", "effect": "deny" }, { "action": "shell", "resource": "*git add --all*", "effect": "deny" }, { "action": "shell", "resource": "*--no-verify*", "effect": "deny" }, { "action": "shell", "resource": "*.env*", "effect": "deny" }, { "action": "shell", "resource": "*creds*", "effect": "deny" }, { "action": "shell", "resource": "*.config/aredel*", "effect": "deny" }, { "action": "shell", "resource": "*user_profile/data*", "effect": "deny" }, { "action": "shell", "resource": "*users.db*", "effect": "deny" }, { "action": "shell", "resource": "*.ssh/*", "effect": "deny" }, { "action": "shell", "resource": "*.gnupg*", "effect": "deny" }, { "action": "shell", "resource": "*.docker/config.json*", "effect": "deny" }, { "action": "shell", "resource": "*opencode/auth.json*", "effect": "deny" }, { "action": "shell", "resource": "*opencode.db*", "effect": "deny" } ] }

Строка в AGENTS.md. Добавлена в список Always файла AGENTS.md, который читают и OpenCode, и Claude Code:

vi AGENTS.md

- Search file contents with `git grep -n`: it reads tracked files only. `grep -rl` for file names is fine; `grep -r` with lines asks, because it also reads ignored data.

Тесты

Условия теста

Тесты шли 24.10.2026 с OpenCode 2.1.3 и бесплатной моделью OpenCode.

Проектом был временный репозиторий с копией набора правил OpenCode 4: отслеживаемые src/app.php и tools/hello.py, фиктивный .env и игнорируемый private/visits.log с фиктивным IP-адресом и словом setAside.

Строки и запросы

Каждый запрос просил один вызов оболочки с командой из своей строки.

Запуски не получали ввода (< /dev/null), поэтому не могли ответить на запрос: спросил означает, что OpenCode запросил разрешение, а запуск его отклонил.

Как читать результаты

Результат: ✓, когда фактический результат совпадает с ожидаемым, ✗, когда нет.

ТестКомандаОжидалосьФактическиРезультат
1grep -rln setAside .выполняетсявыполнился: два имени файлов, без строк

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:grep -rln setAside .Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Команда выполнилась без запроса и перечислила ./src/app.php и ./private/visits.log.

Почему: Разрешение grep -rl* идёт после ask *grep -r*, поэтому побеждает. С -l grep печатает только имена файлов: имя игнорируемого лога видно, а его содержимое нет, как и у инструмента glob.

2grep -rn setAside .спроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:grep -rn setAside .Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode asked about the command, and the run rejected it. Первый запуск этого теста ничего не доказал: модель вызвала несуществующий инструмент и остановилась, поэтому его повторили.

Почему: grep -rn совпадает с ask *grep -r* и ни с одним из разрешений только по именам. При выводе строк он напечатал бы и игнорируемый лог, включая фиктивный IP-адрес.

3git grep -n setAsideвыполняетсявыполнился: только src/app.php

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:git grep -n setAsideThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Команда выполнилась без запроса и напечатала src/app.php:3:function setAside($block) { return $block; }. Игнорируемый лог, где тоже есть это слово, не искали, и фиктивного IP-адреса в ответе нет.

Почему: git grep читает только отслеживаемые файлы, поэтому игнорируемые данные остаются вне поиска, а ни одно правило с ним не совпадает, поэтому он выполняется.

4git grep --no-index -n setAsideспроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:git grep --no-index -n setAsideThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: --no-index заставляет git искать по папке как grep -r, включая игнорируемые файлы, поэтому совпадает новый ask *--no-index*.

5git grep --untracked --no-exclude-standard -n setAsideспроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:git grep --untracked --no-exclude-standard -n setAsideThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: Вместе эти два флага заставляют git искать и по игнорируемым файлам. Совпадает новый ask *--no-exclude-standard*.

6git diff --no-index /dev/null private/visits.logспроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:git diff --no-index /dev/null private/visits.logThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: Тот же ask *--no-index*. Это команда, которая напечатала перенесённый фиктивный лог в примере aredel.

7python3 -c "print(2 + 2)"спроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:python3 -c "print(2 + 2)"Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: Совпадает новый ask *python3 -c*.

8python3 - <<'PY' print(2 + 2) PYспроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:python3 - <<'PY' print(2 + 2) PYThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о части python3 - <<'PY' вместе с её телом, и запуск отклонил запрос.

Почему: Heredoc идёт за python3 - через пробел, поэтому совпадает новый ask *python3 - *.

9node -e "console.log(2 + 2)"спроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:node -e "console.log(2 + 2)"Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: Совпадает новый ask *node -e*.

10php -r 'echo 2 + 2;'спроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:php -r 'echo 2 + 2;'Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: Совпадает новый ask *php -r*.

11python3 tools/hello.pyвыполняетсявыполнился, без запроса

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:python3 tools/hello.pyThen reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: Скрипт выполнился без запроса и напечатал hello from a script.

Почему: Файл скрипта — не встроенный код: ни одно правило не совпадает, и разрешение оболочки из набора правил OpenCode 4 даёт ему выполниться, как и раньше.

12echo '--- ftp reachability now'спроситспросил

Запрос: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it:echo '--- ftp reachability now'Then reply with the output, or with the exact error text. Do not use any other tool."

Что произошло: OpenCode спросил о команде, и запуск её отклонил.

Почему: Текст совпадает с *ftp *. Набор правил 4 сохраняет это ложное срабатывание: оно стоит одного безобидного запроса, а сужение правила рискует пропустить настоящий FTP-клиент.

До и после. Те 15 команд дня, проверенные по каждому списку правил так, как это делает OpenCode: каждая часть командной строки отдельно, побеждает последнее совпавшее правило. Этот подсчёт сделан скриптом, который применяет списки правил, а не запусками OpenCode.

ПравилаЗапросыКакие именно
Набор правил OpenCode 315 из 15все
Набор правил OpenCode 411 из 159 поисков по содержимому, curl, echo с Python
Набор правил OpenCode 4 и строка в AGENTS.md2 из 15curl к хостингу и встроенный Python: оба должны спрашивать

Что остаётся открытым. Набор правил 4 меняет запросы на несколько известных пробелов:

Как тестировать deny-правило

Другие тесты на этой странице. Эта глава проверяет deny-правила для логов, инструмент за инструментом.

Такие же запуски дали тесты site.env, таблицы вариантов A, B и C, а также тесты локального curl.

Что проверяется. Останавливает ли deny-правило каждый способ, которым агент может доставить содержимое защищённого файла в модель: инструмент read, инструмент grep, инструмент glob, оболочку и файл, прикреплённый вами. Правило проходит тест, когда агент отвечает ошибкой прав доступа, а содержимое файла ни разу не появляется в его ответе.

Зачем фиктивные данные. Тест, который успешно прочитал файл, уже отправил его в модель. Поэтому тесты идут на временном репозитории с той же раскладкой папок и одной поддельной строкой лога. Её адрес 203.0.113.7 — из диапазона для документации: если он появился в ответе, файл прочитан; если нет — не прочитан.

Подготовка. У временного репозитория намеренно нет .gitignore: инструмент grep в OpenCode пропускает файлы, которые игнорирует git (тесты site.env), поэтому .gitignore спрятал бы лог от grep ещё до проверки любого правила.

Создайте репозиторий и фиктивный лог:

mkdir -p /tmp/denytest/container/tools/analyze_logs/site_logs/prod/access && cd /tmp/denytest && git init -q

printf "203.0.113.7 - - [22/Oct/2026] \"GET /ai/ HTTP/1.1\" 200\n" > container/tools/analyze_logs/site_logs/prod/access/aredel.com.access.log

Напишите две конфигурации. opencode.before.jsonc содержит правила, которые есть в репозитории сегодня; opencode.after.jsonc добавляет проверяемое deny-правило:

vi opencode.before.jsonc

{ "$schema": "https://opencode.ai/config.json", "permissions": [ { "action": "shell", "resource": "*", "effect": "ask" }, { "action": "read", "resource": "*.env", "effect": "deny" }, { "action": "read", "resource": "*creds*", "effect": "deny" } ] }

vi opencode.after.jsonc

{ "$schema": "https://opencode.ai/config.json", "permissions": [ { "action": "shell", "resource": "*", "effect": "ask" }, { "action": "read", "resource": "*.env", "effect": "deny" }, { "action": "read", "resource": "*creds*", "effect": "deny" }, { "action": "read", "resource": "container/tools/analyze_logs/site_logs/*", "effect": "deny" } ] }

Перед каждым тестом скопируйте названную им конфигурацию в opencode.jsonc. Каждый запрос велит модели использовать ровно один инструмент и процитировать ошибку инструмента, чтобы ответ показал, что сделал инструмент. Запуски ниже: 22.10.2026, OpenCode 2.1.2, на бесплатной модели OpenCode.

Тест 1 — инструмент read до правила. Базовая линия: доказывает, что агент может прочитать файл сегодня, так что поздний отказ — дело правила, а не опечатка в пути.

cp opencode.before.jsonc opencode.jsonc

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the read tool exactly once on container/tools/analyze_logs/site_logs/prod/access/aredel.com.access.log. Then reply with the IP address it contains, or with the exact error text the tool returned. Do not use any other tool."

Тест 2 — инструмент read после правила. Само правило: тот же запрос, но deny-правило на месте.

cp opencode.after.jsonc opencode.jsonc

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the read tool exactly once on container/tools/analyze_logs/site_logs/prod/access/aredel.com.access.log. Then reply with the IP address it contains, or with the exact error text the tool returned. Do not use any other tool."

Тест 3 — инструмент grep после правила. Покрывает ли правило чтения ещё и поиск внутри файла.

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the grep tool exactly once with pattern 203\.0\.113 and path container/tools/analyze_logs. Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool."

Тест 4 — инструмент glob после правила. Остаются ли видны имена файлов в папке.

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the glob tool exactly once with pattern container/tools/analyze_logs/site_logs/**/*. Then reply with the file paths it returned, or with the exact error text. Do not use any other tool."

Тест 5 — cat в оболочке после правила. Покрывает ли правило чтения оболочку. В этой конфигурации оболочка стоит на ask.

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the shell tool exactly once to run: cat container/tools/analyze_logs/site_logs/prod/access/aredel.com.access.log . Then reply with the output, or with the exact error text. Do not use any other tool."

Тест 6 — ваше собственное вложение после правила. Останавливает ли правило чтения файл, который вы прикрепили сами.

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free -f container/tools/analyze_logs/site_logs/prod/access/aredel.com.access.log "Do not use any tools. What IP address is in the attached file? If no file content was given to you, answer exactly NOT GIVEN."

Тест 7 — инструмент grep после добавления запрета grep. Исправление для теста 3: добавьте { "action": "grep", "resource": "*", "effect": "deny" } в конфигурацию after, затем повторите команду теста 3.

vi opencode.after.jsonc

cp opencode.after.jsonc opencode.jsonc

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the grep tool exactly once with pattern 203\.0\.113 and path container/tools/analyze_logs. Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool."

Тест 8 — лог вне проекта. Структурное исправление: вынесите лог из репозитория и запретите внешнюю папку, { "action": "external_directory", "resource": "/tmp/outside/*", "effect": "deny" }, затем попросите файл инструментом read и инструментом grep.

mkdir -p /tmp/outside/site_logs && mv container/tools/analyze_logs/site_logs/prod /tmp/outside/site_logs/

vi opencode.after.jsonc

cp opencode.after.jsonc opencode.jsonc

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the read tool exactly once on /tmp/outside/site_logs/prod/access/aredel.com.access.log. Then reply with the IP address it contains, or with the exact error text the tool returned. Do not use any other tool."

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the grep tool exactly once with pattern 203\.0\.113 and path /tmp/outside/site_logs. Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool."

Тест 9 — инструмент read, сессия начата в подпапке. Совпадает ли правило, когда OpenCode запущен из container/, а не из корня репозитория. Конфигурация в корне репозитория всё ещё находится, но пути теперь относительны container/.

cp opencode.after.jsonc opencode.jsonc

cd container && timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the read tool exactly once on tools/analyze_logs/site_logs/prod/access/aredel.com.access.log. Then reply with the IP address it contains, or with the exact error text the tool returned. Do not use any other tool."

Затем измените правило на *analyze_logs/site_logs/* в opencode.after.jsonc, скопируйте его снова и повторите ту же команду:

vi opencode.after.jsonc

Тест 10 — правило ask под --auto. Останавливается ли --auto, который пропускает запросы подтверждения, на правиле ask. Оставьте лог вне проекта из теста 8 и смените эффект его правила external_directory с deny на ask. Затем выполните запрос с grep без --auto и с ним. Запуск 22.10.2026, OpenCode 2.1.3.

vi opencode.after.jsonc

cp opencode.after.jsonc opencode.jsonc

timeout 180 opencode run --standalone -m opencode/deepseek-v4.1-flash-free "Use the grep tool exactly once with pattern 203\.0\.113 and path /tmp/outside/site_logs. Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool." < /dev/null

timeout 180 opencode run --auto --standalone -m opencode/deepseek-v4.1-flash-free "Use the grep tool exactly once with pattern 203\.0\.113 and path /tmp/outside/site_logs. Then reply with the matching lines it returned, or with the exact error text. Do not use any other tool." < /dev/null

Сводка.

ТестСпособ входаКонфигурацияОжидалосьФактическиРезультат
1инструмент readbeforeсодержимоесодержимоебазовая линия
2инструмент readafterзапретзапретпройдено
3инструмент grepafterзапретсодержимоепробел
4инструмент globafterтолько имятолько имяпринято
5оболочка, catafterне запускается без запросаотклонено, некому спроситьпройдено в скриптах
6вложениеafterне отправленоотправленопробел
7инструмент grepafter + запрет grepзапретинструмента grep нетпройдено
8read и grepлог вынесен, запрет externalзапретзапретпройдено
9инструмент read из подпапкиafter; затем ведущая *запретсодержимое; затем запретпробел; пройдено с *
10инструмент grepлог вынесен, external ask; затем --autoспроситспросил; затем содержимоепройдено; пробел под --auto

Когда запуск ничего не доказывает. Если запуск висит до таймаута, модель выбрала инструмент с эффектом ask, и ответить было некому. Это ни провал, ни успех: повторите с более узким запросом. Если ответ описывает файл, не цитируя инструмент, модель угадала; повторите и требуйте точный вывод инструмента. Если запуск из скрипта ничего не печатает до таймаута, opencode run ждёт ввода: закройте его через < /dev/null.

Чек-лист

Официальные источники:

Автор статьи: Клавдия Кондакова