- BrainTools - https://www.braintools.ru -
В первой статье [1] я разбирал, почему ~/.claude нельзя просто положить в облачную папку, и шесть граблей, на которые наступил, пока делал синхронизацию через git. Тогда движок жил в основном на стендах: одна рабочая машина и разыгранные сценарии с подменённым $HOME.
С тех пор к хранилищу подключился ноутбук, и месяц я работал на двух машинах вперемешку: начинал разговор на одной, продолжал на другой. Стендов стало десять вместо трёх, проверок — 269 вместо 64. Почти каждый новый стенд появился после того, как что-то сломалось в жизни. Ниже — пять новых граблей и то, чем их пришлось закрывать.
Нумерацию продолжаю с первой статьи.
В первой статье я написал, что при одновременной работе с двух машин «порядок реплик может поехать». Это оказалось неправдой, и неправдой интересной.
Claude Code не читает транскрипт по порядку строк. У каждой записи есть uuid и parentUuid, и разговор собирается обратным обходом: от последней записи файла по цепочке родителей к началу. Всё, что в эту цепочку не попало, в контекст не идёт, даже если лежит в файле.
Я проверил это на живом транскрипте: из 3665 записей с uuid цепочка от конца проходит через 1764 — меньше половины файла. Остальное — брошенные ветки: человек вернулся назад и переписал вопрос, а прежнее продолжение осталось лежать. Это норма, а не поломка: в одной моей сессии таких развилок 28, в другой 13.
Отсюда два следствия.
«Файл целый» и «сессия откроется тем же разговором» — разные утверждения. Второе проверяется только обходом цепочки.
merge=union не теряет строки, но может потерять разговор. Если две машины продолжили одну сессию врозь, git склеит обе версии, но продолжится одна ветка — та, куда ведёт последняя запись. Вторая останется в файле невидимой.
Первая мысль была — «выбрать правильную ветку»: переставить строки так, чтобы последней оказалась нужная. Не выйдет: время записей в транскрипте не монотонно, а в одном файле последняя строка и самая поздняя запись вообще не совпадали. Подменять timestamp — значит врать в данных.
Поэтому движок ветку не выбирает, а разносит: ccsync branches показывает сессии, где разошлись ветки, ccsync split превращает каждую ветку в отдельную сессию. У каждой получившейся ровно один хвост, и как бы Claude Code ни искал точку старта, он найдёт нужный разговор. Автоматически при pull это не делается: молча переделывать чужие сессии нельзя.
Отличить склейку двух машин от обычного «вернулся и переписал» по одному файлу невозможно — структурно это одно и то же. Различает масштаб: переписанный вопрос — одна-две записи, чужая ветка — кусок работы. В движке порог — шесть записей.
Эта грабля стоила мне полдня. Я поработал на десктопе: поправил CLAUDE.md, скрипт-обёртку и статус-строку. Потом открыл ноутбук, который не включал несколько дней, и первым делом сделал push all. Ноутбук честно отдал в хранилище свои версии этих файлов — старые — и откатил всё, что я сделал на десктопе. Вернул коммитом-откатом.
Причина в том, что для файлов вне JSON у движка было одно правило: «локальное отличается от хранилища — отдаём». А отличаться оно может по двум противоположным причинам: его здесь поправили — или его здесь не подтянули.
Различить их можно, только если помнить, на чём машина в прошлый раз сошлась с хранилищем. Теперь каждая машина хранит такой снимок — для настроек, MCP-серверов, плагинов, CLAUDE.md, статус-строки и скриптов обвязки. И решение становится трёхсторонним, как в git:
|
|
локальное = снимок |
правили здесь |
снимка нет |
|---|---|---|---|
|
pull |
взять из хранилища |
не трогать, сказать |
взять, прежнее — в |
|
push |
не отдавать: в хранилище новее |
отдать |
отдать |
Строка «не отдано — в хранилище другая версия» теперь не ошибка [2], а ровно то поведение [3], которого не хватало: машина видит, что отстала, и ничего не затирает.
Попутно выяснилось худшее: --dry-run был холостым только на словах. Флаг был объявлен, но push его не читал вовсе — push --dry-run коммитил и отправлял в хранилище по-настоящему. А pull соблюдал его только для MCP и обвязки; остальное писал на машину, вплоть до удаления транскриптов по отметкам забытых сессий. Теперь холостой прогон идёт на одноразовой копии хранилища: тот же код, что и боевой запуск, но без отправки, а в конце печатается, что ушло бы или пришло бы. Для «посмотреть, что уедет» это обязательное свойство — иначе пробный запуск и есть тот самый запуск.
В первой статье правило было простое: секретам в репозитории не место. Через неделю я его отменил: ключи API нужны на каждой машине, а переносить их руками надоело. Теперь они ездят, но только зашифрованными age.
Устроено так:
явный реестр — какие файлы с ключами возятся;
шифротекст лежит в репозитории, файл шифруется сразу для всех получателей;
у каждой машины свой ключ, публичные части — в общем списке. Общего секрета на все машины нет, и потеря одной машины не компрометирует остальные;
приватный ключ в git не попадает никогда.
Отдельно — транскрипты. Ключи попадают в диалог сами собой: вставил в чат, вывел cat-ом. Перед выгрузкой они заменяются на {{SECRET:метка}} — по точному совпадению с известными машине секретами и по форме (sk-…, ghp_…, JWT). Замена односторонняя: транскрипт — это история, а не конфиг. На живом транскрипте проверка дала 17 вхождений полного ключа до и 0 после.
А теперь грабля. Когда подключается новая машина, ccsync init кладёт ей пустой шаблон файла секретов — одни комментарии, «впишите сюда». В хранилище тот же файл лежит зашифрованным, с настоящими ключами.
Дальше на новую машину ставится age, и первый же push видит: снимка для файла нет, расшифровать хранилище машина ещё не может (её ещё не добавили в получатели). Старый код в этом месте шёл в ветку «зашифровать локальное» — и клал пустой шаблон поверх настоящего файла. А остальные машины при pull видели: «у меня файл не правили, в хранилище новее» — и послушно заменяли свои ключи пустышкой. Ключи терялись на всех машинах сразу.
Нашлось это до потерь: я собирался выдать ключ ноутбуку и сначала прогнал сценарий на стенде. Воспроизведение выглядело так:
отдано: ['.claude/ccsync-secrets.env']
в хранилище теперь: ENC[# Локальные секреты этой машины]
Исправление — инвариант «шаблон никогда не заменяет секрет». Файл из одних комментариев при существующем шифротексте не отдаётся, а при pull заменяется настоящим (раньше движок считал шаблон «правкой здесь» и настоящий ключ не отдавал). Первый вариант исправления закрыл только случай «расшифровать нечем» — вторую ветку той же дыры («расшифровать можно, но снимка нет») нашло ревью, о нём ниже.
И порядок, который из этого следует: перешифровать секреты под новую машину может только та, что уже умеет их расшифровывать. Новая добавляет себя в получатели — и ждёт, пока старая сделает push.
Все сценарии до сих пор начинались с чистого ~/.claude. А потом я собрался подключить мак, на котором Claude Code стоит давно — со своей памятью [4], скиллами и MCP-серверами. И это, кстати, основной сценарий для любого, кто возьмёт шаблон себе: чистый ~/.claude бывает только у тех, кто Claude Code ещё не пользовался.
Я разыграл это на стенде: живое хранилище плюс «давняя машина» со старыми версиями тех же скиллов, своим CLAUDE.md, своей памятью и своим MCP.
Хорошая новость: общему хранилищу подключение не вредит. Одноимённый скилл со старой версией не перезаписал свежий — побеждает более новый по времени изменения. Настройки, CLAUDE.md и модель остались общими: первый push после pull отдаёт только то, что поменяли на машине после pull (спасибо снимкам из восьмой грабли).
Плохая — на самой машине и на пути её данных наружу нашлись три дыры:
MEMORY.md перезаписывался без копии. С ccsync он генерируется из общих фактов, а до ccsync Claude Code писал его сам, и там может лежать вся память машины. Теперь прежний сохраняется в MEMORY.md.bak. Свой, сгенерированный, движок узнаёт по непереводимой метке в файле — по заголовку нельзя, он переводится, и со сменой языка каждый pull начал бы плодить копии.
Перенос памяти делал чужие заметки общими. У факта в хранилище есть scope — на каких машинах он верен. А у заметки, написанной до ccsync, его нет, и факт без scope движок считает global. Перенеси такую заметку командой — и пути мака разъедутся на все машины. Теперь заметки без scope не переносятся, а перечисляются: «разметьте и перенесите руками».
MCP-сервер машины нельзя было пометить заранее. Сервер, завязанный на программы мака, при первом push уехал бы на все машины. Пометить его «только здесь» до этого было нельзя: команда отказывала серверу, которого ещё нет в хранилище. Теперь можно.
И важное наблюдение про порядок шагов. Хуки синхронизации начинают работать только после перезапуска Claude Code — а значит, между первым pull и перезапуском есть безопасное окно, когда с машины ничего само не уедет. В него и помещается разбор её прошлого: какие MCP только её, какие скиллы нужны всем, что из памяти переносить и с какой пометкой. Этот разбор я вынес в инструкцию подключения отдельным шагом — и делается он вместе с Claude, по одной заметке.
У движка две версии: моя приватная и публичный шаблон, который выпускается из неё скриптом с обезличиванием. В публичной есть свой стенд — «чистый старт»: две одноразовые машины проходят путь «первая машина → вторая».
Правка про память без scope прошла все мои стенды. А на выпуске публичный стенд упал: «факт памяти уехал в хранилище — ожидали да, получили нет».
Он был прав. На первой машине хранилища запрет не имеет смысла: других машин ещё нет, её память и есть вся память, и именно её надо перенести. Мои стенды этот случай не покрывали — у меня хранилище давно существует, и «первой машины» в моей жизни больше не бывает. Теперь правило такое: на первой машине заметки без scope переносятся как раньше, со второй — только вручную.
Урок, который я вынес: стенды публичной версии проверяют то, что приватные проверить не могут в принципе — сценарии, из которых ты сам уже вырос. Гонять их нужно при каждом выпуске, даже если «правка чисто внутренняя».
Обвязка вне ~/.claude. Часть инструментов Claude Code живёт снаружи: скрипт-чистилка транскриптов в ~/.local/bin, его таймер в ~/.config/systemd/user. Хранилище возит и их — но только по явному списку. В этих каталогах лежит вся остальная жизнь машины: монтирование облака, синхронизация буфера обмена, иконки в трее. Им в общем репозитории не место.
Пустые сессии не уезжают вовсе. Связь с claude.ai заводит сессии-заглушки без единой реплики, а сессия, где успели нажать только слэш-команду, выглядит так же. Раньше они ехали в git, а потом убирались по коммиту на сессию.
Проект можно исключить целиком. Там, где сессии заводит расписание (у меня так живёт утренняя сводка погоды), каждый запуск — новая сессия, и помечать их по одной бессмысленно.
Цифры: 10 стендов, 269 проверок, плюс три проверки самого шаблона — чистый старт, полнота перевода и английский вывод.
Отдельно скажу про ревью. Все правки этого месяца писались в паре с Claude Code, и перед тем как считать правку готовой, я отдавал её второму агенту — ревьюеру, которому поставлена одна задача: искать тихие отказы, проглоченные ошибки и места, где данные теряются без единого слова. Дважды он нашёл настоящее: вторую ветку дыры с секретами и нечитаемый MEMORY.md, который движок перезаписал бы без копии. Оба раза — с воспроизведением, а не «мне кажется».
Формат .jsonl по-прежнему недокументирован, а седьмая грабля показала, насколько нетривиально он устроен внутри.
Одновременная работа в одной сессии с двух машин не поддерживается: получатся две ветки, продолжится одна. branches и split помогут разобрать, но не предотвратят.
В ежедневной работе обкатан только Linux. Мак и Windows проверены стендами, живые машины — следующий шаг, и подозреваю, что следующая статья будет про них.
Репозиторий тот же: github.com/Cha1000000/claude-code-sync [5]. Всё описанное выше уже в нём, вместе с инструкцией для «машины с прошлым».
Автор: Voland_CoderMan
Источник [6]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36124
URLs in this post:
[1] первой статье: https://habr.com/ru/articles/1077424/
[2] ошибка: http://www.braintools.ru/article/4192
[3] поведение: http://www.braintools.ru/article/9372
[4] памятью: http://www.braintools.ru/article/4140
[5] github.com/Cha1000000/claude-code-sync: https://github.com/Cha1000000/claude-code-sync
[6] Источник: https://habr.com/ru/articles/1087346/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1087346
Нажмите здесь для печати.