PunchKano begins diphtheria immunisation in five Kano LGsRTP DesportoFrancisco Cabral na final de pares em HangzhouESPN DeportesGrecia derrotó a Alemania por primera vez en la historia y golpeó a Klopp en su debut ante su públicoThe Jerusalem PostIsrair awaits final approval for Tokyo, Miami flights, adds new European destinations for 2027Daily MaverickWe worried AI would make things up, we should also worry when it doesn’tInquirerRains to continue in Visayas, Mindanao due to ITCZ until Sept. 30Bollywood HungamaVinod Kapri's Pyre to release in theaters on October 23, 2026BlickNächstes Amt abgelegt: Jens Spahn zieht sich aus Haushaltsausschuss zurückRapplerPhilippines should fix tax gaps and procurement, not raise tax rates – WBSportstarIndia in Athletics LIVE Updates, Asian Games 2026: Vithya Ramraj breaks National Record to win 400m hurdles bronze; Javelin throw final at 4:45 PM ISTIl Fatto QuotidianoMorto Stefano Milani, il tifoso del Milan diventato famoso su X. Da Bertolucci a Valenti: “Ha lottato come nessuno, mai un passo indietro”7sur7“Pourquoi je perds toujours?”: quand André Agassi taquine Alexander Zverev
The Daily Newsstand · Free, Always
Monday, September 28, 2026

Синхронизация Claude Code, месяц спустя: что ещё сломалось, когда машин стало две по-настоящему

Translate

В первой статье я разбирал, почему ~/.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

взять из хранилища

не трогать, сказать

взять, прежнее — в .bak

push

не отдавать: в хранилище новее

отдать

отдать

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

Попутно выяснилось худшее: --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 стоит давно — со своей памятью, скиллами и 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. Всё описанное выше уже в нём, вместе с инструкцией для «машины с прошлым».

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.