Как дать кодинг-агенту секрет, не вставляя его в промпт

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

Ситуация всегда одна и та же. Агент на середине задачи, ему нужно обратиться к вашему тестовому API Stripe, прогнать миграцию на staging-базе или запушить в приватный реестр. Он просит ключ. Ключ лежит у вас в менеджере паролей, агент ждёт, и самое быстрое, что можно сделать на свете, это вставить его в чат.

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

Мы делаем AgentsRoom, десктопное приложение, которое запускает кодинг-агентов, так что нам пришлось решить это для себя. Мы ошиблись три раза в сентябре 2026 года. Вот правило, к которому мы в итоге пришли, три ошибки, которые его породили, и как применить его независимо от того, пользуетесь вы нашим приложением или нет.

Короткий ответ

  • Секрет попадает к агенту через переменную окружения и никак иначе. Ни через сообщение, ни через тикет, ни через навык, ни через флаг командной строки.
  • Агент пишет $NAME, а не значение. Оболочка подставит его, когда команда начнёт выполняться.
  • Сохраните значение один раз, вне репозитория, зашифрованным связкой ключей вашей ОС, и везде дальше ссылайтесь на него по имени.
  • Секрет, прошедший через переписку, сгорел. Смените его, а потом прибирайтесь.
  • Маскирование это страховочная сетка, а не план. Оно ловит то, что выглядит как ключ, а не пароль от базы, который читается как обычное слово.

Почему эта вставка хуже, чем кажется

Происходят три вещи сразу, и очевидна только первая.

Ключ уходит к провайдеру модели. Эту часть видят все. Он теперь лежит в теле запроса на чужой инфраструктуре, внутри переписки, которую вы не можете отредактировать.

Ключ уходит на ваш диск, открытым текстом. Claude Code, Codex, Antigravity CLI и остальные держат транскрипт переписки, чтобы её можно было продолжить. Этот файл лежит в вашей домашней папке, незашифрованным, ровно столько, сколько CLI хранит свою историю. Поищите один раз строку sk- в своих транскриптах и посмотрите, что найдётся.

Ключ расползается вбок. Переписка это память агента, поэтому всё, что копирует переписку, копирует и ключ: возобновлённая сессия, форкнутый агент, субагент, запущенный с контекстом родителя, резюме, записанное в файл проекта. Мы видели, как ключ путешествовал из сообщения в чате в описание тикета, а оттуда в первое сообщение второго агента, и никто при этом не сделал ничего плохого.

И привычная страховочная сетка не держит. Маскирование вывода работает по шаблону: sk-…, glpat-…, форма JWT. Оно не может знать, что thursday-harbour-71 это пароль от вашей staging-базы. В собственном приложении мы маскируем распознанные значения в выводе терминала и всё равно считаем это сеткой под трапецией, а не самой трапецией.

Правило одной двери

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

Всё остальное это буквальный текст. Сообщение, которое вы набираете, описание тикета, файл навыка, системный промпт, флаг командной строки: всё это читает человек или модель, поэтому секрет, записанный там, это опубликованный секрет.

Командная строка заслуживает отдельной фразы, потому что кажется безопаснее сообщения в чате, а на деле не безопаснее. Значение в --api-key=… видно каждому процессу на машине через ps, оно попадает в историю вашей оболочки, его записывает всё, что логирует запущенную команду, и оно всплывает в диагностических инструментах, которые показывают вам строку процесса. Переменная окружения одного процесса читается этим процессом и пользователем root. Поверхность меньше, удобство то же.

Агенту, к слову, значение не нужно никогда. Ему нужно написать вот это:

curl -H "Authorization: Bearer $STRIPE_SECRET_KEY" https://api.stripe.com/v1/charges

Оболочка подставляет $STRIPE_SECRET_KEY при выполнении. Агент написал семнадцать символов и знак доллара. В его транскрипте лежат семнадцать символов и знак доллара.

Как это устроено в нашем приложении

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

Одно хранилище, один файл. AgentsRoom держит секреты в одном файле в вашей домашней папке, в режиме 0600, зашифрованном связкой ключей операционной системы и подтверждаемом при каждой записи. Ничего не синхронизируется на сервер и ничего не уходит на другой компьютер: секрет принадлежит машине, которая его хранит. Если связка ключей недоступна, сохранение завершается ошибкой, а не откатывается к открытому тексту.

Вы сохраняете значение один раз. Команды и подключения > Секреты, в любом проекте. Имя (буквы, цифры, точка, дефис, подчёркивание, 64 символа, чтобы его можно было использовать как имя переменной окружения) и значение, набранное в поле пароля. Появившаяся строка показывает имя и его ссылку, {{secret:STRIPE_SECRET_KEY}}, и кнопку, которая копирует ссылку. Кнопки «показать» нет, кнопки «скопировать значение» нет, потому что нет ни одного пути в коде, который вернул бы значение в интерфейс.

Вы ссылаетесь на него там, где собирается окружение. Изменить агента > Дополнительно > Переменные окружения, или настройки проекта, или настройки провайдера, или командная строка dev-терминала:

STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}}

При запуске основной процесс подставляет значение в окружение терминала, который он собирается создать. Интерфейс его не видит, агент его не видит, телефон его не видит. Страница «Менеджер секретов» показывает эти экраны.

Агент узнаёт имена, а не значения. Агент, подключённый к нашим инструментам MCP, может вызвать secrets_list и получить список имён, которые есть на этой машине. Этого достаточно, чтобы он приносил пользу: он может сказать «у вас сохранён STRIPE_SECRET_KEY, добавьте STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}} в окружение этого агента, и я буду использовать $STRIPE_SECRET_KEY». Прочитать значение он не может, даже выполняя команду, которая этим значением пользуется.

Три ошибки, которые мы выпустили

Записанное правило выглядит очевидным. На самом деле это рубец от трёх багов, все в сентябре 2026 года, все найдены пользователями.

Ссылка, процитированная в тикете, выходила подставленной

19 сентября. Robert задокументировал соответствие в описании тикета, совершенно разумное STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}}, чтобы тот, кто возьмёт тикет, знал, что нужно настроить. Прочитанный через API, тикет возвращал ссылку нетронутой. Но агент, запущенный из этого тикета, получал в первом сообщении значение.

Причиной была деталь внутренней сантехники с вполне реальным последствием. Бриф тикета становится первым промптом агента, а тот путешествовал внутри переменной окружения команды запуска. Наш резолвер подставлял значения во все переменные окружения, поэтому подставил и промпт. Ключ ушёл на диск и к провайдеру модели. Смена ключа, обойти это было нечем.

Исправление состояло в том, чтобы сделать команду запуска агента буквальной: когда окружение несёт идентификатор агента, переменная с первым промптом остаётся нетронутой. Dev-команда с той же переменной не имеет идентификатора агента и подставляется как раньше. Один побочный эффект мы приняли сознательно: ссылка, набранная в опциях CLI агента, тоже больше не подставляется, и это правильно, ведь командная строка всё равно попадает в ps и в журналы.

Урок: дверь должна быть узкой намеренно. «Подставлять везде, где это может пригодиться» это ровно тот путь, которым секрет оказывается там, где его быть не должно.

Ссылка, вставленная в сообщение, которая никогда не подставляется

22 сентября. Cs040618 сообщил о противоположной проблеме: «я вставляю ссылку в переписку, а агент не может понять, что это за ключ».

Это не баг. Сообщение по замыслу является буквальным текстом, и это ровно то исправление, которое мы сделали тремя днями раньше. Но ничто в приложении об этом не говорило в момент самого действия, а агент, получив {{secret:…}} в сообщении, понятия не имел, что отвечать.

Три исправления, и все о том, чтобы показать, где находится дверь: подсказка, которая срабатывает, когда вы вставляете ссылку в поле ввода, и указывает на переменные окружения; пояснение над списком секретов, которое называет три поля, где ссылка подставляется, и говорит «никогда в сообщении»; и строка в собственных инструкциях агента, которая говорит ему, что отвечать, когда ссылка приходит в сообщении.

«Настройки > Секреты» не существовало

28 сентября. Агент Mohammad завёл отчёт: пользователь говорит, что добавил OTPIQ_API_KEY в Настройки > Секреты, сохранял его несколько раз за полчаса, а secrets_list по-прежнему возвращает те же три старых секрета.

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

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

Урок здесь вообще не о секретах: неверный путь в справочном тексте это баг безопасности. Он уводит пользователя к ближайшему полю, которое выглядит подходящим.

Если вы не пользуетесь AgentsRoom

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

  • CLI менеджера паролей. op run -- claude с 1Password или эквивалент в Bitwarden и Doppler: инструмент читает ссылки из шаблона, вставляет настоящие значения в окружение дочернего процесса, и ничего не пишется на диск.
  • direnv с .envrc в gitignore. Просто, локально, по папкам. Добавьте .envrc и .env* в .gitignore до первого значения, а не после.
  • Файл .env, который агент никогда не читает. Держите его вне репозитория и загружайте в оболочке, которая запускает агента. Если файл лежит внутри репозитория, агент рано или поздно его откроет, процитирует в сообщении, и вы вернётесь туда, откуда начали.
  • Облачные учётные данные: берите короткоживущие. GitHub OIDC в CI вместо долгоживущих ключей AWS и рабочая нагрузка с собственной идентичностью вместо файла ключа сервисного аккаунта.

Что бы вы ни выбрали, добавьте это правило в инструкции агента, чтобы он перестал спрашивать. Хватит одного абзаца в CLAUDE.md или AGENTS.md: никогда не спрашивать значение учётных данных, никогда не записывать его в файл, использовать $NAME и говорить пользователю, какую переменную настроить. Мы публикуем небольшой навык, который говорит ровно это, и в нём пять строк.

Если ключ уже прошёл через переписку

По порядку, и первый шаг не опционален.

  1. Смените его. Отправленный промпт не отзывается. Считайте значение скомпрометированным и выпустите новое.
  2. Найдите транскрипты. CLI хранят файлы переписок в вашей домашней папке. Поищите в них старое значение и удалите или обрежьте то, что совпало.
  3. Проверьте места, которые копируют текст. История менеджера буфера обмена, тикет или issue, где агента инструктировали, любое резюме, которое агент записал в репозиторий, и git log -p, если агент коммитил, пока держал ключ.
  4. И только теперь стройте дверь. Новое значение за переменной окружения, ссылка по имени и обновлённые инструкции агента, чтобы следующий не спрашивал.

Шаг 4 последним, потому что чистый механизм вокруг утёкшего ключа это всего лишь аккуратнее прибранная утечка.

Вопросы, которые люди задают на самом деле

Почему нельзя вставлять ключ API в промпт для ИИ-агента?

Потому что промпт сохраняется и пересылается. Claude Code, Codex и остальные CLI пишут переписку в файл транскрипта на вашем диске, а само сообщение уходит к провайдеру модели. Дальше ключ оказывается ещё и в истории вашего буфера обмена, в любом журнале, который дублирует сообщение, и в повторных попытках и субагентах, копирующих переписку. Маскирование по шаблону ловит то, что похоже на sk- или glpat-, но не пароль от базы, который выглядит как обычное слово.

Как кодинг-агент пользуется секретом, который он не может прочитать?

Через переменную окружения. Вы кладёте значение в переменную окружения того процесса, в котором работает агент, и агент пишет в команде $STRIPE_SECRET_KEY вместо значения. Оболочка подставляет его в момент выполнения команды. Агент никогда не видит сами символы, поэтому они не попадают ни в его переписку, ни в его транскрипт.

Можно ли вместо этого передать секрет аргументом командной строки?

Можно, но не нужно. Командная строка видна каждому процессу на машине через ps, она попадает в историю оболочки, в журналы процессов и в любой диагностический инструмент, который перечитывает запущенную команду. Переменная окружения одного процесса читается этим процессом и пользователем root, а это куда меньшая поверхность.

Где AgentsRoom хранит секреты, которыми пользуются агенты?

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

Что будет, если вставить ссылку на секрет в сообщение агенту?

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

Ключ уже прошёл через переписку. Что делать?

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

Если коротко

Агенту не нужно читать ваш ключ. Ему нужен процесс, в окружении которого ключ уже лежит.

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

А когда вы всё-таки ошибётесь, а это случится, сначала смените ключ и только потом прибирайтесь.

Скачать AgentsRoom

Запускай всех своих ИИ-агентов во всех проектах из одного окна.

БесплатноСкачать AgentsRoom

Приложение-компаньон: следите за агентами на ходу

Используйте Claude, Codex, Antigravity CLI или другого поставщика AI.

Установить расширение
Chrome Web Store

Отправляйте баги и запросы прямо в ваш публичный бэклог.

Мульти-проекты
Мульти-провайдер
Мульти-агенты
Статус онлайн
Diff и коммиты
Мобильное приложение
Live-превью
Команды агентов
Тесты в браузере
Разработка от backlog
Библиотека промптов
Библиотека навыков
Все функции

Читать далее