Я распаковал исходник Claude Code v2.1.88. Половина того, что про него пишут — миф

от автора

Почти всё, что я считал про устройство Claude Code изнутри, оказалось упрощением. Я распаковал бандл версии 2.1.88 — около 1884 файлов в src/ — и пошёл сверять, что из общеизвестного правда, а что нет. Ниже восемь мест, где расхожее мнение разошлось с кодом, и под конец одна вещь про безопасность, которая мне самому не понравилась.

Сразу про метод и происхождение кода. Читаемым он стал случайно: в npm-релизе v2.1.88 уехал source map, а через него восстанавливаются исходные файлы — с именами и комментариями. Это давно не секрет. Глазами строка за строкой я его не читал — для такого объёма нереально. Я прошёлся по ключевым модулям и каждый вывод сверял с кодом: какая функция, какая константа. Поэтому дальше попадаются имена вроде AUTOCOMPACT_BUFFER_TOKENS и queryLoop — найдите их в своей копии и проверьте меня. И ещё: это разбор того, как оно устроено, а не «слив потрохов». Внутренние промпты дословно не цитирую, а куски, которые работают только во внутренних сборках Anthropic, помечаю отдельно.

Миф 1. «Агент рекурсивно вызывает сам себя на каждый результат инструмента»

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

Ядро — один while (true) внутри асинхронного генератора queryLoop (src/query.ts). Рекурсии нет. Между проходами цикл таскает один и тот же изменяемый объект State: в точке, где решает продолжить, целиком перезаписывает state = { ... } и делает continue. Стек не растёт вглубь.

Звучит как придирка к словам, но на практике это важно. Раз это итерация, а не вложенный вызов, то всё, что вы настраиваете — бюджеты, таймауты, лимиты ходов — считается на один проход цикла, а не на кадр стека. «Один ход» здесь буквально и есть «один проход». Когда я перестал думать про Claude Code как про рекурсивного агента и начал — как про долгий цикл с состоянием, к нему подошли те же приёмы, что и к любому долгому циклу: считать бюджет на каждый шаг и смотреть, что поменялось между шагами.

Миф 2. «Когда контекст переполняется, он просто обрезается»

Тут самое интересное. Контекстом занимается не одна функция «выкинуть старое», а пять отдельных механизмов, выстроенных от дешёвого к дорогому: snip, microcompact, context-collapse, autocompact и reactive. Порядок не случайный. Каждый следующий стоит после предыдущего ровно затем, чтобы — если ранний уже освободил место — поздний просто ничего не делал. В комментарии так и написано: collapse гоняют до autocompact, чтобы при удаче до autocompact дело не дошло.

Дешёвые механизмы точечно выкидывают старые результаты инструментов. Дорогой — autocompact — отдельным запросом к модели сжимает всю историю в сводку. Порог, на котором он включается, считают как эффективный размер окна контекста минус AUTOCOMPACT_BUFFER_TOKENS, то есть минус 13 000 токенов запаса под саму сводку.

А вот из-за чего я полез копать глубже. В autocompact зашит тихий предохранитель: после трёх неудачных сжатий подряд (MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3) он выключается до конца сессии. Молча. В комментарии к этой константе видно, зачем её добавили: попадались сессии с полусотней неудач подряд, до 3272 в одной, и это давало порядка 250 тысяч лишних запросов к API в день суммарно по всем пользователям. То есть сессия, которая «вроде шла нормально часами», вполне могла часть этого времени держаться на одном точечном выкидывании, без полноценного сжатия — и из интерфейса вы бы этого не заметили.

Миф 3. «При сжатии всегда остаётся хвост последних сообщений как есть»

Я был уверен, что сжатие сохраняет последние несколько сообщений дословно, а трогает только то, что подальше. Для полного autocompact это не так.

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

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

Миф 4. «Инструменты запускаются после того, как модель договорила»

Нет — инструмент уходит в работу ещё до того, как модель закончила фразу.

Меня это подловило не сразу. Как только в потоке появляется блок tool_use и вы за эту долю секунды не нажали отмену, StreamingToolExecutor его уже запустил. Модель в этот момент ещё дописывает текст, а правка файла на диске уже случилась. За параллельностью следит CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY со значением 10 по умолчанию: то, что безопасно запускать разом, идёт пачкой, остальное — по очереди.

И есть нюанс, на котором я однажды залип на полчаса. У исполнителя свой дочерний контроллер отмены, и если падает один из параллельных инструментов — скажем, Bash — он мгновенно гасит соседей, но сам ход при этом не обрывает. Соседу, которого прибили за компанию, прилетает что-то вроде «отменён, потому что рядом упал другой вызов». И вот сидишь потом и гадаешь, почему команда, которая вообще ни от чего не зависела, вдруг не отработала — а она, оказывается, зависела от соседа.

Миф 5. «Одно сообщение модели — это один ответ, и stop_reason говорит, был ли вызов инструмента»

Тут два факта, которые экономят часы отладки.

Первый простой: на каждый блок Claude Code шлёт отдельное сообщение ассистента, а не одно на весь ответ. Текст, размышление, вызов инструмента — каждый блок уходит своим сообщением, как только дописан.

Второй коварнее. В момент закрытия блока stop_reason всегда null. Настоящее значение приедет позже, отдельным событием, и его впишут задним числом — прямой правкой уже отправленного сообщения. В коде по этому поводу честный комментарий: «stop_reason === ‘tool_use’ is unreliable». Поэтому цикл ему и не верит: чтобы понять, был ли вызов инструмента, он смотрит на факт — пришёл блок tool_use или нет. Кто писал свою обёртку над таким стримом и ловил на stop_reason гонки — теперь знает, откуда они.

Миф 6. «Права — это просто цепочка user -> project -> local -> policy»

Слои действительно есть, и звучит логично: чей слой выше, того правило и побеждает. Я тоже так думал. На самом деле порядок выстроен не по источнику, а по строгости.

deny рубит всё, даже bypassPermissions. Дальше, по убыванию строгости, идут точечные ask правила и проверки безопасности, затем сам обход, затем разрешения, и лишь то, что нигде явно не разрешили, в самом конце доходит до вопроса пользователю. Запрет всегда оказывается сильнее обхода.

Но интереснее не порядок, а зоны, которые обход не пробивает вообще. Даже в bypassPermissions, который вроде бы и означает разрешаю всё, не спрашивай, правки в .git/, .claude/, .vscode/ и шелл-конфигах всё равно упрутся в подтверждение. Логика тут простая, пусти агента править собственные настройки без спроса и он сам себе выпишет пропуск из песочницы прав. В ту же сторону работают значения по умолчанию: пока инструмент явно не объявил, что только читает и его можно запускать параллельно, система считает, что он пишет и параллелить его нельзя.

Миф 7. «Субагент — это просто ещё один Claude рядом»

Субагент — это изолированный форк, и изоляция жёстче, чем рисуется. Ему выдают свой agentId, отдельную копию списка прочитанных файлов и пустую память.

Но самое любопытное тут. У обычного субагента setAppState по умолчанию пустой, то есть менять состояние приложения он не может. А раз не может — ему сразу выставляют флаг разрешения не спрашивать, и дальше всё следует само: показать диалог прав фоновый субагент физически не может, поэтому любой его ask тихо превращается в deny. Если вы поручили субагенту задачу, которая упёрлась в подтверждение, он не станет дожидаться вас, получит отказ и поедет дальше, будто вы сами сказали «нет».

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

Миф 8. «Расширений у Claude Code — пяток стандартных хуков»

Механизмов расширения пять: MCP-серверы, плагины, скиллы, хуки и slash-команды. Плагин стоит особняком — это зонтик, под который можно сложить сразу остальные четыре. А дальше начинаются цифры, ради которых я и копал.

Хуков не пять «канонических», как обычно перечисляют в гайдах (SessionStart, PreToolUse, PostToolUse, Stop, UserPromptSubmit), а целых 28 — среди прочего там есть события совместной работы (teammate), задач, смены рабочего каталога, изменений в файлах. И сам контракт хука устроен не так, как принято думать. Расхожее «вернул не ноль — значит ошибка» неверно: исходов на самом деле три. Ноль означает, что всё прошло штатно. Двойка — это жёсткая блокировка: действие отменяют, а модель получает объяснение почему. А любой другой ненулевой код считается мягкой ошибкой — stderr покажут вам, но сессия продолжится. Из-за этой тройственности появилась довольно смешная защита: перед запуском отдельно проверяют, на месте ли вообще папка плагина. Иначе хук дёрнул бы python3 <нет файла>.py, тот сам свалился бы с кодом 2, и без этой проверки один-единственный пропавший файл намертво заклинил бы Stop и UserPromptSubmit — сессия просто не смогла бы завершиться.

Скиллы — отдельная история, и всё про них объясняет одна константа: SKILL_BUDGET_CONTEXT_PERCENT = 0.01, то есть один процент контекста на весь список скиллов. В этот процент влезает только шапка — имя, описание, триггер; тело SKILL.md грузится, лишь когда скилл реально позвали. Вот почему можно навешать десятки скиллов и почти не платить за них контекстом: пока их не вызвали, для контекста их как будто нет.

И последняя мелочь, которая убивает иллюзию изоляции. «Пространство имён» у MCP-инструментов — это громкое название для обычного строкового префикса mcp__server__tool: имя сервера и имя инструмента просто склеивают, всё лишнее (не буква, не цифра, не _ и не -) меняют на подчёркивание, и по этой склеенной строке раздают права. Никакой настоящей изоляции за «пространством имён» нет.

Напоследок неприятное про безопасность

Я думал, что вопрос про доверие к папке, который Claude Code задаёт при запуске это вообще первое, что он делает. Оказалось, нет: вопрос «доверяете ли этой папке?» всплывает заметно позже. К этому моменту запуск уже прокрутил приличный кусок кода, судя по исходнику выполняется примерно тысяча строк, там же честный комментарий, что по безопасности здесь тонко. А тонко вот в чём: .claude/settings.json к этому моменту уже прочитан, и лежит он в той самой папке, которой вы ещё не сказали «доверяю».

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

Что со всем этим делать

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

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

Вопрос к тем, кто давно сидит на Claude Code: что из этого расходится с тем, что вы видите на практике, — или, наоборот, наконец объясняет давно замеченную странность?

ссылка на оригинал статьи https://habr.com/ru/articles/1043904/