● LIVE· № 001 · SANITIZE ВСЁ, ЧТО УХОДИТ В GIT: ЧИСТКА ПУБЛИЧНЫХ РЕПО ОТ УТЕЧЕК ИДЕНТИЧНОСТИ · 2026.05.11· № 002 · IMAGEGEN-MCP: СВОЙ MCP-СЕРВЕР ДЛЯ ОБЛОЖЕК БЛОГА · 2026.05.11· № 003 · SMART PASTE: ЧИЩУ МУСОР ИЗ ТЕРМИНАЛА ПЕРЕД ВСТАВКОЙ ОДНИМ ХОТКЕЕМ · 2026.05.10· № 004 · CLAUDE CODE TEAM TELEMETRY: ЦЕНТРАЛИЗОВАННАЯ СТАТИСТИКА ПО КОМАНДЕ · 2026.05.07· № 005 · ГАЙД ДЛЯ НОВИЧКОВ: КАК ВКАТЫВАТЬСЯ В CLAUDE CODE · 2026.05.06· 11 СТАТЕЙ · 0 ЧЕРНОВИКОВ
RU / EN
·10 МИН

Sanitize всё, что уходит в git: чистка публичных репо от утечек идентичности

Identity-leaks — самый дешёвый класс утечки чтобы пропустить. Работодатель в git log, codename в комментарии, LAN IP в README. История одного дня охоты на собственные следы и пять слоёв защиты, чтобы это не повторилось.

Боль

Ты пушишь dotfiles в публичный GitHub. Кажется — "конфиги, скрипты, ничего секретного". Через полчаса понимаешь:

  • В git log светится твой work-email [email protected], потому что какой-то старый merge-коммит из PR притащил domain работодателя.
  • Имя коллеги [email protected] тоже в metadata — он когда-то прислал PR в твой fork, и форк сохранил его commits с work-domain.
  • В gitleaks.toml, который ты сам же написал чтобы блокировать утечки, ты литерально указал regex с твоим work-domain — и тем самым задокументировал этот domain в public репо.
  • В CLAUDE.md описаны internal codenames твоего home-lab сервера и его RFC1918 адрес.

Большая часть этого — не классические секреты. Не API-key, не private-key. Это identity-leak: твоё лицо, твоё место работы, твоя домашняя инфра, твой workflow. Большинство автоматических scanner-ов это не ловит — там built-in detector-ы под AWS/Stripe/JWT, а не "имя коллеги в attributable form".

И самая обидная часть — ты до push-а думал, что чистишь.

Что нашёл за один день

Зашёл с одной задачей ("положу скрипт в dotfiles") и нашёл семь разных классов утечки:

  1. Work-email в commit metadata двух разных людей (мой + коллеги).
  2. Domain имени работы в литералах gitleaks-конфига, который я сам же написал чтобы блокировать leak.
  3. Internal codenames моего home-lab — упоминания в comment-ах документации.
  4. RFC1918 LAN IP твоего домашнего сервера в примерах конфига.
  5. Path-leak — упоминания путей к локальным secret-директориям в documentation как пример (сами по себе не секрет, но указывают, что секреты у меня хранятся ровно там, без encryption-layer).
  6. Stale local refs от старого git fetch pull/N/head:pr-N — после force-push на main эти ветки тянут pre-rewrite коммиты с work-email обратно в локальный pack.
  7. Lockfile sha512 хеши, содержащие подстроку codename как часть base64-блоба → false-positive нового codename-правила.

Каждый — отдельный класс. Один scanner ни одно из них не ловит полностью.

Что применили — defense in depth

Пять слоёв. Каждый ловит свой класс. Нет single source of truth, и не должно быть.

Слой 1. Git identity по директории

Дефолтный user.email в ~/.gitconfig — public noreply:

[user]
  name = your-handle
  email = [email protected]

Work identity активируется только внутри ~/projects/work/:

[includeIf "gitdir/i:~/projects/work/"]
  path = ~/.gitconfig-work

~/.gitconfig-work (отдельный файл, не в публичных dotfiles):

[user]
  name = Real Name
  email = [email protected]

Эффект: коммитишь в work-проекте → автор work. Коммитишь где угодно ещё → автор public. Никаких больше "ой забыл переключить identity".

Слой 2. Pre-commit gitleaks scan

Через core.hooksPath = ~/.config/git/hooks — глобально, один раз на машину. Все existing и future репо проходят через тот же хук, без per-repo install-hooks.sh.

Hook прогоняет gitleaks protect --staged против двух конфигов:

  • ~/.config/gitleaks.toml — public-safe правила. Extends built-in (~150 detector-ов: AWS, Stripe, GCP, GitHub, Slack, OpenAI, Anthropic, etc.) плюс generic кастомы: RFC1918 ranges, sk-ant- / sk-proj- / Bearer, PEM private keys, ~/.claude/secrets/ paths. Файл живёт в dotfiles, виден всем кто склонит репо. В нём только generic паттерны — нет привязки к конкретному work-domain или codename.
  • ~/.config/gitleaks.local.toml — personal правила. Лежит outside dotfiles, gitignored. В нём твой work-domain, codename home-server, личный domain. Этот файл — сам по себе leak, если попадёт в public репо, поэтому к нему отдельный gitignore + reminder в installer.

Hook в pseudocode:

gitleaks protect --staged --config ~/.config/gitleaks.toml         # public rules
[ -f ~/.config/gitleaks.local.toml ] && \
  gitleaks protect --staged --config ~/.config/gitleaks.local.toml # personal rules

Per-repo opt-out: git config hooks.skipLeaks true (например, для work-репо, где work-email в каждом коммите — норма).

Слой 3. Pre-push semantic review через Claude (opt-in)

Regex не ловит контекстные leaks: "client X said …", "Project X milestone", "internal API call shape". Это semantic, regex по ключевым словам сломается на false positives.

Pre-push hook прогоняет outgoing diff через claude --print --model haiku со строгим prompt-ом — ищет NDA names, client codenames, real-name leaks, business logic. Cheap (haiku ~$0.0001 per push), быстро (~1-3s).

Opt-in: git config hooks.claudeReview true per репо. Иначе вообще не запускается.

Слой 4. Cleanup historical leaks (один раз, тяжело)

git filter-repo — modern замена filter-branch. Запускаешь с --mailmap или --email-callback чтобы переписать email metadata, --message-callback чтобы вычистить нежелательные строки из commit messages.

Mailmap пример:

your-handle <[email protected]> <[email protected]>

Затем git push --force --all + --tags. Дальше обязательно:

  1. Re-add origin — git filter-repo strips origin remote by default ("safety"); нужно вернуть git remote add origin <url>.
  2. Удалить stale remote-tracking refs — git update-ref -d refs/remotes/origin/<stale-branch> для каждой ветки, которой нет на remote. git fetch --prune их не убирает, если ветка была удалена раньше.
  3. GC — git reflog expire --expire=now --all && git gc --prune=now --aggressive. Без этого orphan-коммиты остаются локально и потом тянутся обратно при следующем merge / cherry-pick.
  4. Force-push дельные branches и tags, не только default.

И не забудь: после force-push GitHub продолжает резолвить старые SHA по прямому URL https://github.com/<user>/<repo>/commit/<old-sha> ещё ~30-90 дней до cache-GC. Полный wipe = только delete + recreate репо.

Слой 5. Personal-config files outside dotfiles

~/.config/gitleaks.local.toml, ~/.gitconfig-work, ~/.claude/secrets/*.env — никогда не в публичных dotfiles. Только в private storage. Installer (install.sh) при первом запуске scaffold-ит template из публичного .example-файла в нужное место с явным warning-ом:

⚠  ACTION REQUIRED: edit your personal gitleaks rules
   ~/.config/gitleaks.local.toml
   ...
   To open now:
       ${EDITOR:-vi} ~/.config/gitleaks.local.toml

Без явного warning человек забудет про этот шаг и пушнёт следующий коммит без personal-guard. Прошёл сам по граблям.

Подводные камни (на которые наступили)

  • filter-repo removes origin remote. Выглядит как баг, но это safety feature (нельзя случайно push pre-rewrite на upstream). Re-add manually.
  • Stale remote-tracking refs не убираются git fetch --prune. Если ты fetched pull/N/head:pr-N — этот ref остаётся локально и тянет pre-rewrite metadata. Чистится только git update-ref -d refs/remotes/origin/pr-N.
  • gitleaks [allowlist] syntax зависит от scope. Top-level [allowlist] применяется ко всем правилам. Per-rule allowlist пишется как [rules.allowlist] сразу после [[rules]] блока — не [[rules.allowlist]] (array syntax падает с decoder error).
  • Lockfile sha512 хеши содержат base64-blob-ы из произвольных букв. Codename из 3 символов почти гарантированно столкнётся с хешем какого-нибудь NPM-транзитива. Решение: добавь lockfile-пути в allowlist каждого короткого codename-правила.
  • Короткий codename collision с English словом. Если твой codename совпадает с реальным словом (или его подстрокой) — allowlist может покрыть compound-формы, но не standalone слово. Принимаешь false-positive в prose против missed-leak в коде. Это правильный trade-off, но об этом надо сказать commit-author-у явно через документацию правила.
  • GitHub contributor cache пересчитывается через несколько минут после push, но full removal из contributor-page после force-push занимает до часа. Дай ему отстояться.
  • Force-push не GC-ит remote SHA. Старые коммиты доступны по URL ~30-90 дней. Если нужен немедленный wipe — delete + recreate репо.

Что измеримо

После полного прохода (один день, 19 моих репо):

  • 0 упоминаний work-email в commit metadata любого репо.
  • 0 упоминаний work-domain в blob-ах истории (через gitleaks detect).
  • 0 internal codenames во всех 22 моих репо. Единственные hits — в npm sha512 hashes в lockfile-ах, allowlist-ed по path.
  • 2 gitleaks config-а loaded каждым commit-ом — public-safe в dotfiles + personal local outside.
  • 2 git identity автомат-переключаются по gitdir — work-стол vs остальное.

Главный takeaway

Public commits живут вечно. git push --force прячет reachable refs из default-view, но SHA-кэш на GitHub держит старые объекты ещё ~90 дней. Если что-то ушло в public — это уже архив для всех, кто успел склонить, fork-нуть, GH archive проиндексировать. Sanitize до push-а — это не paranoia, это единственный effective момент.

Identity-leak — самый дешёвый класс утечки чтобы пропустить. Никто не написал тебе API-key в plaintext. Зато:

  • работодатель в git log (один old merge commit когда-то),
  • codename в comment (один документ, который ты сам написал),
  • LAN IP в README (один пример, который казался безобидным).

Каждое — мелочь. Все вместе — точная картина того, где ты работаешь, какая инфра у тебя дома, и какой у тебя workflow.

Защита — слои, не точечные правила:

  1. Identity по dir — два контекста, не один общий.
  2. Pre-commit scan с двумя конфигами (public + personal local outside dotfiles).
  3. Pre-push semantic review для контекстных leaks.
  4. Historical cleanup через filter-repo + careful ref management.
  5. Personal config outside dotfiles — никогда сами эти правила в public репо.

Любой один слой проседает — другой ловит. И не забудь поставить ${EDITOR:-vi} reminder в installer, иначе будущий ты забудет первый шаг и запушит следующий коммит без guard-а.

P.S. — самый надёжный layer

По-хорошему, всё, что выше — компенсация за то, что мы вообще смешали личное и рабочее на одной машине. Самая надёжная защита — разные девайсы: рабочий ноут для work-репо, рабочей почты, корпоративных VPN, NDA-всего; личный — для своих репо, dotfiles, экспериментов, AI-инструментов.

Разделение физическое:

  • Нет shared ~/.gitconfig, который надо переключать includeIf-ом — у каждого девайса свой.
  • Нет shared ~/.ssh/ и ~/.aws/, где можно случайно подписать work-commit личным ключом и наоборот.
  • Нет shared clipboard / pasteboard / cloud-sync, через которые snippet с work-domain доезжает до твоего личного fish-конфига.
  • Нет Claude Code / Codex / Copilot с доступом к work-кодовой базе и одновременно к личным репо в recent files.
  • Browser, password manager, мессенджеры — отдельные профили или отдельные приложения.

Промежуточный вариант: разные OS-юзеры на одной машине

Если два устройства — слишком дорого (новый ноут + второй monitor / mic / клавиатура × NaN), есть средний путь: один MacBook, но два пользователя системы. Personal user и Work user. На macOS — Fast User Switching.

Что получаешь:

  • Отдельный $HOME у каждого юзера → отдельные ~/.gitconfig, ~/.ssh/, ~/.aws/, ~/.claude/, ~/.codex/, ~/Library/Application Support/. Никакого пересечения.
  • Отдельный Keychain — пароли, токены, SSH-ключи изолированы.
  • Отдельные браузерные сессии (не профили в одном Chrome, а вообще разные user-instances).
  • Отдельный clipboard / Раycast clipboard history.
  • Disk Encryption + автоматический lock при switch → если кто-то получит доступ к одному юзеру, другой остаётся за encryption-стеной.

Что теряешь:

  • Переключение — заёб. Fast User Switch всё-таки не моментальный, и каждый раз надо помнить, кто ты сейчас. После пары недель устаёшь и начинаешь "ну ладно, сделаю эту маленькую задачу под personal-юзером, потому что лень переключать". Тут вся защита и сыпется.
  • Двойные ресурсы — две Docker Desktop-инстанции, два IDE setup-а, два набора расширений. На 16GB RAM-машине очень больно.
  • Лицензии на софт часто per-user (Spotify, JetBrains, etc.).
  • Resume/sleep state иногда теряется при switch.

Это на ступеньку выше одного юзера с гигиеной, но на ступеньку ниже двух физических девайсов. Хорошо подходит когда:

  • работаешь временно или part-time (полноценный work-laptop не нужен),
  • хочешь чёткую изоляцию данных без покупки железа,
  • есть мощная machine (32GB+ RAM, M-series Apple Silicon) которая тащит два user-context-а одновременно.

Пять слоёв выше — это потому что я этого не сделал ни в одном из двух смыслов: ни two-device, ни two-user-account. Просто один пользователь, одна машина, всё вместе. Они работают, но они дорогие: время, attention, periodic audits, edge-case-ы.

Tldr — шкала вариантов

  1. Один юзер + гигиена (five layers above) — дёшево, требует discipline и periodic audit.
  2. Два OS-юзера на одной машине — изоляция данных, теряешь UX, ёкаешь переключаться.
  3. Два физических девайса — золотой стандарт, но удваиваешь capex и периферию.

Чем выше по шкале — тем меньше edge-case-ов и audit-времени, но больше денег / friction. Чем ниже — наоборот: дёшево и удобно, но один забытый git config user.email и работодатель в git log.

Если стоит выбор — двигайся вверх. Будущий ты скажет спасибо. xD