Боль
Ты пушишь 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") и нашёл семь разных классов утечки:
- Work-email в commit metadata двух разных людей (мой + коллеги).
- Domain имени работы в литералах gitleaks-конфига, который я сам же написал чтобы блокировать leak.
- Internal codenames моего home-lab — упоминания в comment-ах документации.
- RFC1918 LAN IP твоего домашнего сервера в примерах конфига.
- Path-leak — упоминания путей к локальным secret-директориям в documentation как пример (сами по себе не секрет, но указывают, что секреты у меня хранятся ровно там, без encryption-layer).
- Stale local refs от старого
git fetch pull/N/head:pr-N— после force-push на main эти ветки тянут pre-rewrite коммиты с work-email обратно в локальный pack. - 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 rulesPer-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. Дальше обязательно:
- Re-add origin —
git filter-repostripsoriginremote by default ("safety"); нужно вернутьgit remote add origin <url>. - Удалить stale remote-tracking refs —
git update-ref -d refs/remotes/origin/<stale-branch>для каждой ветки, которой нет на remote.git fetch --pruneих не убирает, если ветка была удалена раньше. - GC —
git reflog expire --expire=now --all && git gc --prune=now --aggressive. Без этого orphan-коммиты остаются локально и потом тянутся обратно при следующем merge / cherry-pick. - 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-reporemoves origin remote. Выглядит как баг, но это safety feature (нельзя случайно push pre-rewrite на upstream). Re-add manually.- Stale remote-tracking refs не убираются
git fetch --prune. Если ты fetchedpull/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.
Защита — слои, не точечные правила:
- Identity по dir — два контекста, не один общий.
- Pre-commit scan с двумя конфигами (public + personal local outside dotfiles).
- Pre-push semantic review для контекстных leaks.
- Historical cleanup через
filter-repo+ careful ref management. - 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 — шкала вариантов
- Один юзер + гигиена (five layers above) — дёшево, требует discipline и periodic audit.
- Два OS-юзера на одной машине — изоляция данных, теряешь UX, ёкаешь переключаться.
- Два физических девайса — золотой стандарт, но удваиваешь capex и периферию.
Чем выше по шкале — тем меньше edge-case-ов и audit-времени, но больше денег / friction. Чем ниже — наоборот: дёшево и удобно, но один забытый git config user.email и работодатель в git log.
Если стоит выбор — двигайся вверх. Будущий ты скажет спасибо. xD
