【週末お掃除】git に溜まった不要な操作履歴やデータを削除してストレージ容量を節約しようと試行錯誤した記録
これなに
昨今 AI によって git 管理の在り方自体見直され始めていますが、まだまだみなさん使っているであろう git について。
.git には過去の履歴や不要データがたくさん蓄積していき、気付けばかなり肥大化していてストレージを圧迫していることがあります。
自分のマシンは以下のような感じでかなり肥大化していてしんどいのでお掃除しようと思ったので、その作業記録を個人のメモとしてまとめました。
個人用のメモなのでいろいろ不足していたりすることもありますが、必要な人は参考にしていただけたらと思います。
対象読者
.git によってストレージが食われすぎていてしんどい自分のようなストレージにお金かけられない人たち
実行環境
macOS 27 Golden Gate 27.0.1
⚠️ 注意
後述の手順を実行すると、完全に「元に戻せない(Undoできない)」状態になります。
本来は間違えてブランチを消しても reflog から復元できますが、後述するコマンド群を実行した後はリポジトリが極限まで軽量化される代わりに、過去のミスを救済するデータもすべて消去されます。大容量ファイルの削除作業の「最後の仕上げ」としてのみ実行してください。
作業を実行する前に、現在のリポジトリのバックアップをとるなどして安全策をとるようにしてください。
先にオチだけ
結論だけ先に書くと、最終的に新しいリポジトリ爆誕させてそこにファイルだけ引っ越しました🫠(なんだそりゃ)
とはいうものの、実際何をやったのかをメモしましたので笑いながら読んでください。
初期調査
なんかよくわからないけど GitHub のリポジトリを入れているところがかなり肥大化している模様でした。
タイトルですでに git が問題だということはすでにわかってはいますが、そこに行き着くまでの調査記録を念のため書きます。
ディレクトリのサイズ確認
Applications, Developer などが肥大化するのはまあわかるのですが、 Documents がかなり食っていたのは気になったので調べてみると、 GitHub のディレクトリがかなり食っていることが判明。
du コマンドで調べてみたところこんな感じでした。
% du -sh ~/GitHub/*
1.4G /Users/su3-hokkaido/GitHub/su3-app01
20M /Users/su3-hokkaido/GitHub/su3-app02
5.1G /Users/su3-hokkaido/GitHub/su3-app03
1.4G /Users/su3-hokkaido/GitHub/su3-app04
4.0K /Users/su3-hokkaido/GitHub/su3-app05
4.0K /Users/su3-hokkaido/GitHub/su3-app06
4.0K /Users/su3-hokkaido/GitHub/su3-app07
1.1G /Users/su3-hokkaido/GitHub/su3-app08
4.0K /Users/su3-hokkaido/GitHub/su3-app09
4.0K /Users/su3-hokkaido/GitHub/su3-app10
7.1G /Users/su3-hokkaido/GitHub/su3-app11
207M /Users/su3-hokkaido/GitHub/su3-app12
%
ここからさらに細かい原因を調べるために、特に肥大化が激しい app11 のディレクトリを調べてみることに。
.git 肥大化が判明
さらに分解して調べてみると一つ一つは全然重くなさそう。
% du -sh ~/GitHub/su3-app11/*
1.2M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge01
804M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge02
4.0K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge03
10M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge04
76K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge05
276K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge06
3.4M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge07
28K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge08
18M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge09
148K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge10
216K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge11
60M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge12
12K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge13
1.1M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge14
%
もしかして?と思って .git を調べたらビンゴ。6GBも食っていました。
% du -sh .git/objects
6.3G .git/objects
%
お掃除作業手順(基本編)
ここからは一番大きく、かつ別に履歴は必要としていないリポジトリ(約7GB食っていたリポジトリ)に対して作業していこうと思います。
セットアップ
git-filter-repo をインストールします。(インストール手順はこちら: github.com/newren/git-filter-repo/blob/main/INSTA… )
自分は homebrew 経由でインストールしました。
% brew install git-filter-repo
==> Auto-updating Homebrew...
Adjust how often this is run with $HOMEBREW_AUTO_UPDATE_SECS or disable with
$HOMEBREW_NO_AUTO_UPDATE=1. Hide these hints with $HOMEBREW_NO_ENV_HINTS=1 (see man brew).
==> Downloading ghcr.io
######################################################################### 100.0%
==> Pouring portable-ruby-4.0.9.arm64_big_sur.bottle.tar.gz
==> Downloading bottle manifests
✔︎ Bottle Manifest git-filter-repo (2.47.0) Downloaded 7.3KB/ 7.3KB
==> Fetching downloads for: git-filter-repo
✔︎ Bottle git-filter-repo (2.47.0) Downloaded 101.9KB/101.9KB
==> Pouring git-filter-repo--2.47.0.all.bottle.1.tar.gz
🍺 /opt/homebrew/Cellar/git-filter-repo/2.47.0: 9 files, 361.5KB
%
分析レポート作成
対象リポジトリのディレクトリで git filter-repo --analyze を実行して、お掃除前にどんなものが入っているか確認します。
実際の実行ログは以下のような形です。
% git filter-repo --analyze
Processed 4009 blob sizes
Processed 382 commits
Writing reports to .git/filter-repo/analysis...done.
%
.git/filter-repo/analysis 配下に生成された各ファイルの詳細は省略しますが、ざっとのイメージは以下の通り。
詳しくは大元のリポジトリ git-filter-repo を読んだ方が確実かと思います。
path-all-sizes.txt : 全部のファイル
path-deleted-sizes.txt : 過去に削除されたファイル
directories-all-sizes.txt : 全部のディレクトリ
directories-deleted-sizes.txt : 過去に削除されたディレクトリ
不要オブジェクトを掃除する
実行コマンドは以下の2つ。
1: 過去の操作履歴(リブログ)を今すぐ期限切れにする
git reflog expire --expire=now --all
Gitは、コミットの切り替え(checkout)や打ち消し(reset)、ブランチの削除など、あなたがリポジトリ内で行った「すべての操作履歴」を裏で記録しています。これを reflog と呼びます。
--expire=now: 本来なら一定期間(デフォルトで30〜90日間)保管される操作履歴を、「今すぐすべて期限切れ」にします。
--all: すべてのブランチや参照の操作履歴を対象にします。
2: 不要になったデータ実体を完全に削除し、残ったデータを強力に圧縮する
git gc --prune=now --aggressive
gc(ガベージコレクション)とは Git における「ゴミ集め・掃除」コマンドです。どのコミットやブランチ、操作履歴からもリンクされなくなった孤立したデータを検知して削除します。
--prune=now: 通常、直近2週間に作られたゴミデータは誤操作防止のために残されますが、それを無視して「今あるゴミを今すぐすべて完全削除」します。
--aggressive: 通常の最適化よりも、より時間をかけて強力にファイルを圧縮します。共通するコードやファイル差分を綺麗にまとめて、 .git フォルダの容量を限界まで削ります。
実際の実行ログは以下の通り。
時間の計測はしていないのですが、たぶん実行時間で言うと60分くらいかかったかも。
実行結果
% git reflog expire --expire=now --all
% git gc --prune=now --aggressive
Enumerating objects: 5420, done.
Counting objects: 100% (5420/5420), done.
Delta compression using up to 10 threads
Compressing objects: 100% (5273/5273), done.
Writing objects: 100% (5420/5420), done.
Total 5420 (delta 2924), reused 1282 (delta 0), pack-reused 0 (from 0)
%
お掃除作業手順(発展編)
実行したけれども、100MB だけしか減っていなかった。。。
% du -sh .git/objects
6.2G .git/objects
%
追加調査
git fsck --unreachable --dangling を実行して本当に孤立したゴミ(Dangling object)があるかを改めて確認したところ以下のようになった。
.DS_Store の話しか出てこなくて、消せそうなものが全く無し。
コマンド: git fsck --unreachable --dangling
% git fsck --unreachable --dangling
error: refs/.DS_Store: badRefName: invalid refname format
error: refs/.DS_Store: badRefContent:
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
Checking objects: 100% (5420/5420), done.
Verifying commits in commit graph: 100% (382/382), done.
%
そこでさらに容量を食っている上位10ファイル(コミット)を特定してみます。するといろいろ素材で使っていた動画や画像のコミットが!(これだ!)
コマンド: git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')"
% git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')"
b3d8ee51c7b74ae photos/canon/canon_eos200/IMG_5239.JPG
b41cfacbd8297d5 photos/canon/canon_eos200/MVI_5303.MP4
e875256d0382801 photos/canon/canon_eos200/IMG_5322.JPG
e704d9a01e0a9e2 photos/canon/canon_eos200/IMG_5323.JPG
320f19f210690ae photos/canon/canon_eos200/IMG_5324.JPG
1bb6700c6973c3a photos/canon/canon_eos200/IMG_5325.JPG
d94566c2ad22e3f photos/canon/canon_eos200/IMG_5326.JPG
fc3ab40e7884c53 photos/canon/canon_eos200/IMG_5327.JPG
38e68342661e8ce photos/canon/canon_eos200/MVI_5443.MP4
3ba184a31a09506 photos/canon/canon_eos200/MVI_5526.MP4
%
そんなわけで画像、動画を削除しにいく
--path で指定したフォルダを、歴史上から完全に除外( --invert-paths )してすべてのコミット履歴を自動で書き換えます。
冒頭でインストールした filter-repo を使って削除しに行きます。
git filter-repo --path photos/canon/canon_eos200/ --invert-paths
実行したけど破壊的な上書きだからフレッシュクローンしろと怒られました。
% git filter-repo --path photos/canon/canon_eos200/ --invert-paths
Aborting: Refusing to destructively overwrite repo history since
this does not look like a fresh clone.
(expected at most one entry in the reflog for HEAD)
Please operate on a fresh clone instead. If you want to proceed
anyway, use --force.
%
しかし、ローカルでもう必要なものはバックアップとっているので、気にせず --force で強制実行。
% git filter-repo --path photos/canon/canon_eos200/ --invert-paths --force
NOTICE: Removing 'origin' remote; see 'Why is my origin removed?'
in the manual if you want to push back there.
(was git@github:su3-hokkaido/su3-app11.git)
Parsed 191 commits
New history written in 0.21 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
HEAD is now at abc1234 hogehoge
Enumerating objects: 4728, done.
Counting objects: 100% (4728/4728), done.
Delta compression using up to 10 threads
Compressing objects: 100% (1885/1885), done.
Writing objects: 100% (4728/4728), done.
Total 4728 (delta 2707), reused 4722 (delta 2705), pack-reused 0 (from 0)
Completely finished after 7.05 seconds.
%
これだけでもう 4GB くらい消えました。
Before
% du -sh .git/objects
6.3G .git/objects
%
After
% du -sh .git/objects
2.4G .git/objects
%
最後にもう一度基本編で紹介したコマンドを実行して孤立データを削除しに行きます。
% git reflog expire --expire=now --all
% git gc --prune=now --aggressive
Enumerating objects: 4728, done.
Counting objects: 100% (4728/4728), done.
Delta compression using up to 10 threads
Compressing objects: 100% (4590/4590), done.
Writing objects: 100% (4728/4728), done.
Total 4728 (delta 2700), reused 2008 (delta 0), pack-reused 0 (from 0)
%
が、あまり変わらず。
% du -sh .git/objects
2.4G .git/objects
%
git に再度 push しようにもデータ量が多すぎて、サーバーが耐えきれずに途中で 500 エラーになってしまいました。
% git remote add origin git@github.com:su3-hokkaido/su3-app11.git # filter-repo は origin を削除するので、リモートを再登録する必要あり
% git push -f origin main
Enumerating objects: 4728, done.
Counting objects: 100% (4728/4728), done.
Delta compression using up to 10 threads
Compressing objects: 100% (1890/1890), done.
error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500
send-pack: unexpected disconnect while reading sideband packet
Writing objects: 100% (4728/4728), 2.43 GiB | 21.06 MiB/s, done.
Total 4728 (delta 2700), reused 4728 (delta 2700), pack-reused 0 (from 0)
fatal: the remote end hung up unexpectedly
Everything up-to-date
%
ここから過去のコミットごとに push し直していくという作業をすれば解決する目処はついたものの、そんな気力はなく諦めました。
(スクリプト作って頑張ってもいいけど、労力的に無駄になりそう)
最終奥義
要件的には過去の reflog は全く必要なかったので「もういいや!」となって新しいリポジトリを爆誕させて必要なファイルだけそちらにお引越し。
最初からこっちでよかったね!ハハハハ!!
↓ お掃除前
↓ お掃除後
【週末お掃除】git に溜まった不要な操作履歴やデータを削除してストレージ容量を節約しようと試行錯誤した記録
これなに
昨今 AI によって git 管理の在り方自体見直され始めていますが、まだまだみなさん使っているであろう git について。
.git には過去の履歴や不要データがたくさん蓄積していき、気付けばかなり肥大化していてストレージを圧迫していることがあります。
自分のマシンは以下のような感じでかなり肥大化していてしんどいのでお掃除しようと思ったので、その作業記録を個人のメモとしてまとめました。
個人用のメモなのでいろいろ不足していたりすることもありますが、必要な人は参考にしていただけたらと思います。
対象読者
.git によってストレージが食われすぎていてしんどい自分のようなストレージにお金かけられない人たち
実行環境
macOS 27 Golden Gate 27.0.1
⚠️ 注意
- 後述の手順を実行すると、完全に「元に戻せない(Undoできない)」状態になります。
- 本来は間違えてブランチを消しても reflog から復元できますが、後述するコマンド群を実行した後はリポジトリが極限まで軽量化される代わりに、過去のミスを救済するデータもすべて消去されます。大容量ファイルの削除作業の「最後の仕上げ」としてのみ実行してください。
- 作業を実行する前に、現在のリポジトリのバックアップをとるなどして安全策をとるようにしてください。
先にオチだけ
結論だけ先に書くと、最終的に新しいリポジトリ爆誕させてそこにファイルだけ引っ越しました🫠(なんだそりゃ)
とはいうものの、実際何をやったのかをメモしましたので笑いながら読んでください。
初期調査
なんかよくわからないけど GitHub のリポジトリを入れているところがかなり肥大化している模様でした。
タイトルですでに git が問題だということはすでにわかってはいますが、そこに行き着くまでの調査記録を念のため書きます。
!image.png
ディレクトリのサイズ確認
Applications, Developer などが肥大化するのはまあわかるのですが、 Documents がかなり食っていたのは気になったので調べてみると、 GitHub のディレクトリがかなり食っていることが判明。
!image.png
du コマンドで調べてみたところこんな感じでした。
% du -sh ~/GitHub/*
1.4G /Users/su3-hokkaido/GitHub/su3-app01
20M /Users/su3-hokkaido/GitHub/su3-app02
5.1G /Users/su3-hokkaido/GitHub/su3-app03
1.4G /Users/su3-hokkaido/GitHub/su3-app04
4.0K /Users/su3-hokkaido/GitHub/su3-app05
4.0K /Users/su3-hokkaido/GitHub/su3-app06
4.0K /Users/su3-hokkaido/GitHub/su3-app07
1.1G /Users/su3-hokkaido/GitHub/su3-app08
4.0K /Users/su3-hokkaido/GitHub/su3-app09
4.0K /Users/su3-hokkaido/GitHub/su3-app10
7.1G /Users/su3-hokkaido/GitHub/su3-app11
207M /Users/su3-hokkaido/GitHub/su3-app12
%
ここからさらに細かい原因を調べるために、特に肥大化が激しい app11 のディレクトリを調べてみることに。
.git 肥大化が判明
さらに分解して調べてみると一つ一つは全然重くなさそう。
% du -sh ~/GitHub/su3-app11/*
1.2M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge01
804M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge02
4.0K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge03
10M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge04
76K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge05
276K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge06
3.4M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge07
28K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge08
18M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge09
148K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge10
216K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge11
60M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge12
12K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge13
1.1M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge14
%
もしかして?と思って .git を調べたらビンゴ。6GBも食っていました。
% du -sh .git/objects
6.3G .git/objects
%
お掃除作業手順(基本編)
ここからは一番大きく、かつ別に履歴は必要としていないリポジトリ(約7GB食っていたリポジトリ)に対して作業していこうと思います。
セットアップ
git-filter-repo をインストールします。(インストール手順はこちら: github.com/newren/git-filter-repo/blob/main/INSTA… )
自分は homebrew 経由でインストールしました。
% brew install git-filter-repo
==> Auto-updating Homebrew...
Adjust how often this is run with `$HOMEBREW_AUTO_UPDATE_SECS` or disable with
`$HOMEBREW_NO_AUTO_UPDATE=1`. Hide these hints with `$HOMEBREW_NO_ENV_HINTS=1` (see `man brew`).
==> Downloading https://ghcr.io/v2/homebrew/core/portable-ruby/blobs/sha256:43ff285a82f48c84a06d0676a3800815a19d290d32e44182ddd7a6e95478e18e
######################################################################### 100.0%
==> Pouring portable-ruby-4.0.9.arm64_big_sur.bottle.tar.gz
==> Downloading bottle manifests
✔︎ Bottle Manifest git-filter-repo (2.47.0) Downloaded 7.3KB/ 7.3KB
==> Fetching downloads for: git-filter-repo
✔︎ Bottle git-filter-repo (2.47.0) Downloaded 101.9KB/101.9KB
==> Pouring git-filter-repo--2.47.0.all.bottle.1.tar.gz
🍺 /opt/homebrew/Cellar/git-filter-repo/2.47.0: 9 files, 361.5KB
%
分析レポート作成
対象リポジトリのディレクトリで git filter-repo --analyze を実行して、お掃除前にどんなものが入っているか確認します。
実際の実行ログは以下のような形です。
% git filter-repo --analyze
Processed 4009 blob sizes
Processed 382 commits
Writing reports to .git/filter-repo/analysis...done.
%
.git/filter-repo/analysis 配下に生成された各ファイルの詳細は省略しますが、ざっとのイメージは以下の通り。
詳しくは大元のリポジトリ git-filter-repo を読んだ方が確実かと思います。
- path-all-sizes.txt : 全部のファイル
- path-deleted-sizes.txt : 過去に削除されたファイル
- directories-all-sizes.txt : 全部のディレクトリ
- directories-deleted-sizes.txt : 過去に削除されたディレクトリ
不要オブジェクトを掃除する
実行コマンドは以下の2つ。
1: 過去の操作履歴(リブログ)を今すぐ期限切れにする
git reflog expire --expire=now --all
- Gitは、コミットの切り替え(checkout)や打ち消し(reset)、ブランチの削除など、あなたがリポジトリ内で行った「すべての操作履歴」を裏で記録しています。これを reflog と呼びます。
- --expire=now: 本来なら一定期間(デフォルトで30〜90日間)保管される操作履歴を、「今すぐすべて期限切れ」にします。
- --all: すべてのブランチや参照の操作履歴を対象にします。
2: 不要になったデータ実体を完全に削除し、残ったデータを強力に圧縮する
git gc --prune=now --aggressive
- gc(ガベージコレクション)とは Git における「ゴミ集め・掃除」コマンドです。どのコミットやブランチ、操作履歴からもリンクされなくなった孤立したデータを検知して削除します。
- --prune=now: 通常、直近2週間に作られたゴミデータは誤操作防止のために残されますが、それを無視して「今あるゴミを今すぐすべて完全削除」します。
- --aggressive: 通常の最適化よりも、より時間をかけて強力にファイルを圧縮します。共通するコードやファイル差分を綺麗にまとめて、 .git フォルダの容量を限界まで削ります。
実際の実行ログは以下の通り。
時間の計測はしていないのですが、たぶん実行時間で言うと60分くらいかかったかも。
実行結果
% git reflog expire --expire=now --all
% git gc --prune=now --aggressive
Enumerating objects: 5420, done.
Counting objects: 100% (5420/5420), done.
Delta compression using up to 10 threads
Compressing objects: 100% (5273/5273), done.
Writing objects: 100% (5420/5420), done.
Total 5420 (delta 2924), reused 1282 (delta 0), pack-reused 0 (from 0)
%
お掃除作業手順(発展編)
実行したけれども、100MB だけしか減っていなかった。。。
% du -sh .git/objects
6.2G .git/objects
%
追加調査
git fsck --unreachable --dangling を実行して本当に孤立したゴミ(Dangling object)があるかを改めて確認したところ以下のようになった。
.DS_Store の話しか出てこなくて、消せそうなものが全く無し。
# コマンド: git fsck --unreachable --dangling
% git fsck --unreachable --dangling
error: refs/.DS_Store: badRefName: invalid refname format
error: refs/.DS_Store: badRefContent:
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
Checking objects: 100% (5420/5420), done.
Verifying commits in commit graph: 100% (382/382), done.
%
そこでさらに容量を食っている上位10ファイル(コミット)を特定してみます。するといろいろ素材で使っていた動画や画像のコミットが!(これだ!)
# コマンド: git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')"
% git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')"
b3d8ee51c7b74ae photos/canon/canon_eos200/IMG_5239.JPG
b41cfacbd8297d5 photos/canon/canon_eos200/MVI_5303.MP4
e875256d0382801 photos/canon/canon_eos200/IMG_5322.JPG
e704d9a01e0a9e2 photos/canon/canon_eos200/IMG_5323.JPG
320f19f210690ae photos/canon/canon_eos200/IMG_5324.JPG
1bb6700c6973c3a photos/canon/canon_eos200/IMG_5325.JPG
d94566c2ad22e3f photos/canon/canon_eos200/IMG_5326.JPG
fc3ab40e7884c53 photos/canon/canon_eos200/IMG_5327.JPG
38e68342661e8ce photos/canon/canon_eos200/MVI_5443.MP4
3ba184a31a09506 photos/canon/canon_eos200/MVI_5526.MP4
%
そんなわけで画像、動画を削除しにいく
--path で指定したフォルダを、歴史上から完全に除外( --invert-paths )してすべてのコミット履歴を自動で書き換えます。
冒頭でインストールした filter-repo を使って削除しに行きます。
git filter-repo --path photos/canon/canon_eos200/ --invert-paths
実行したけど破壊的な上書きだからフレッシュクローンしろと怒られました。
% git filter-repo --path photos/canon/canon_eos200/ --invert-paths
Aborting: Refusing to destructively overwrite repo history since
this does not look like a fresh clone.
(expected at most one entry in the reflog for HEAD)
Please operate on a fresh clone instead. If you want to proceed
anyway, use --force.
%
しかし、ローカルでもう必要なものはバックアップとっているので、気にせず --force で強制実行。
% git filter-repo --path photos/canon/canon_eos200/ --invert-paths --force
NOTICE: Removing 'origin' remote; see 'Why is my origin removed?'
in the manual if you want to push back there.
(was git@github:su3-hokkaido/su3-app11.git)
Parsed 191 commits
New history written in 0.21 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
HEAD is now at abc1234 hogehoge
Enumerating objects: 4728, done.
Counting objects: 100% (4728/4728), done.
Delta compression using up to 10 threads
Compressing objects: 100% (1885/1885), done.
Writing objects: 100% (4728/4728), done.
Total 4728 (delta 2707), reused 4722 (delta 2705), pack-reused 0 (from 0)
Completely finished after 7.05 seconds.
%
これだけでもう 4GB くらい消えました。
# Before
% du -sh .git/objects
6.3G .git/objects
%
# After
% du -sh .git/objects
2.4G .git/objects
%
最後にもう一度基本編で紹介したコマンドを実行して孤立データを削除しに行きます。
% git reflog expire --expire=now --all
% git gc --prune=now --aggressive
Enumerating objects: 4728, done.
Counting objects: 100% (4728/4728), done.
Delta compression using up to 10 threads
Compressing objects: 100% (4590/4590), done.
Writing objects: 100% (4728/4728), done.
Total 4728 (delta 2700), reused 2008 (delta 0), pack-reused 0 (from 0)
%
が、あまり変わらず。
% du -sh .git/objects
2.4G .git/objects
%
git に再度 push しようにもデータ量が多すぎて、サーバーが耐えきれずに途中で 500 エラーになってしまいました。
% git remote add origin git@github.com:su3-hokkaido/su3-app11.git # filter-repo は origin を削除するので、リモートを再登録する必要あり
% git push -f origin main
Enumerating objects: 4728, done.
Counting objects: 100% (4728/4728), done.
Delta compression using up to 10 threads
Compressing objects: 100% (1890/1890), done.
error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500
send-pack: unexpected disconnect while reading sideband packet
Writing objects: 100% (4728/4728), 2.43 GiB | 21.06 MiB/s, done.
Total 4728 (delta 2700), reused 4728 (delta 2700), pack-reused 0 (from 0)
fatal: the remote end hung up unexpectedly
Everything up-to-date
%
ここから過去のコミットごとに push し直していくという作業をすれば解決する目処はついたものの、そんな気力はなく諦めました。
(スクリプト作って頑張ってもいいけど、労力的に無駄になりそう)
最終奥義
要件的には過去の reflog は全く必要なかったので「もういいや!」となって新しいリポジトリを爆誕させて必要なファイルだけそちらにお引越し。
最初からこっちでよかったね!ハハハハ!!
↓ お掃除前
!image.png
↓ お掃除後
!image.png
You Deleted the Leaked Key. Git Kept a Copy
You deleted the leaked key. Git kept a copy. 6 commands find it. 1 erases it.
Deleting a secret in a new commit doesn't remove it from Git. The old commits, the reflog and even a dropped stash still hold it. Every run is real, in a throwaway container against my own demo repo. The AWS key shown is fake (made up, never valid).
Rule number one: rotate the key first. Rewriting history doesn't touch clones and forks that already have it.
1. git log -S: pickaxe search, every commit that added or removed a string.
git-scm.com
2. git show: the commit that "deleted" the key still prints it in its diff.
git-scm.com
3. git blame: the commit, author and time behind each line.
git-scm.com
4. git reflog: a reset --hard commit is still in the reflog, and still readable.
git-scm.com
5. git fsck --lost-found: a dropped stash becomes a dangling commit you can still read.
git-scm.com
6. TruffleHog: scans every commit in the history for secrets.
trufflesecurity/trufflehog
7. git filter-repo: the fix. Rotate the key, then rewrite every commit (--replace-text).
newren/git-filter-repo
GitHub's guide: docs.github.com
Where the key should live instead, secrets managers and other DevSecOps tools: stationx.net
Free web-book, Become the Cyber Security Expert the AI Era Demands: app.stationx.net
Music: "Liberation" by Kraftamt, CC BY 3.0, ccmixter.org
#git #devsecops #secrets #appsec #cybersecurity
9 likes · 1 comment · 238 views
API key in Git history? Deleting .env won't fix it #Shorts
You deleted the API key. Git didn't.
Every commit is a snapshot of all your files. Delete .env in a new commit and the old commit still has it: git show HEAD~1:.env prints the key.
The fix, in this order:
1. Rotate the key: revoke it and issue a new one. GitHub's docs and the git-filter-repo manual both say to do this first.
2. Rewrite history: git filter-repo --sensitive-data-removal --invert-paths --path .env, then git push --force --mirror origin. This can't reach other people's clones or forks, and one git pull && git push from an old clone brings the file back. That's why step 1 comes first.
3. Put .env in .gitignore before the first commit. .gitignore doesn't affect files Git already tracks.
On GitHub, push protection for your account is on by default and stops pushes of secrets to public repositories, for the key formats secret scanning recognizes.
Repo, hashes and terminal output in the video come from a real example repo (Git 2.56). The key is fake.
Sources:
- GitHub Docs, Removing sensitive data from a repository: docs.github.com
- git-filter-repo manual: github.com/newren/git-filter-repo/blob/main/Docum…
- GitHub Docs, About push protection: docs.github.com
- GitHub Docs, Supported secret scanning patterns: docs.github.com
- Pro Git, What is Git?: git-scm.com
- gitignore: git-scm.com
Ever committed a secret? Yes or no.
#Git #Programming #Shorts
11 likes · 2 comments · 911 views
Repositório oficial: newren/git-filter-repo
1 like
You Committed .env to GitHub: What to Rotate (Supabase + Next.js)
You pushed, then your stomach dropped: .env is in the commit. Maybe a bot already emailed you. Take a breath — this is recoverable, and panicking into random git commands usually makes it worse.
The single most important thing to understand: deleting the file does not fix anything. Once a secret is in git history, assume it is public forever. The real fix is rotation — making the leaked value useless by replacing it upstream. Purging history is cleanup you do after rotating, not instead of it.
Let's go value by value through a typical Supabase + Next.js .env, sort out what's actually a secret, and rotate only what matters.
First: which of these is actually secret?
A typical .env looks like this:
NEXT_PUBLIC_SUPABASE_URL=https://abcdxyz.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi... # role: anon
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi... # role: service_role
SUPABASE_JWT_SECRET=super-secret-signing-key
DATABASE_URL=postgresql://postgres:PASSWORD@db.abcdxyz.supabase.co:5432/postgres
STRIPE_SECRET_KEY=sk_live_...
RESEND_API_KEY=re_...
Here's the part that saves you hours of unnecessary panic.
Safe by design — do NOT need rotating:
- NEXT_PUBLIC_SUPABASE_URL — your project URL is public. It's in every network request the browser makes.
- NEXT_PUBLIC_SUPABASE_ANON_KEY — the anon (publishable) key is public by design. It ships in your client bundle on purpose. Any NEXT_PUBLIC_ variable is inlined into the browser JavaScript at build time, so it was never a secret to begin with.
The anon key's role is anon. It has no special power on its own — it's just an identity meaning "an unauthenticated (or logged-in) user of this project." What protects your data when that key is used is Row Level Security (RLS). If your tables have RLS enabled with correct policies, a leaked anon key is a non-event. If they don't, that key was already exposed to every visitor of your site — the git commit changed nothing.
Actually secret — rotate every one of these:
- SUPABASE_SERVICE_ROLE_KEY
- SUPABASE_JWT_SECRET
- DATABASE_URL (the Postgres password inside it)
- Every third-party key: Stripe, Resend, OpenAI, etc.
The service_role key is the scary one. Its role is service_role, and it bypasses RLS entirely — full read, write, and delete on every table, plus Storage. It's a server-only secret. It must never appear in a browser, a public repo, or anything with a NEXT_PUBLIC_ prefix. If it leaked, treat it as a full database breach until rotated.
How to tell the two keys apart
Both are JWTs, so they look identical at a glance. Decode the middle segment (base64URL) and read the role claim:
# paste your key in place of PASTE_KEY
echo 'PASTE_KEY' | cut -d. -f2 | base64 -d 2>/dev/null | python -m json.tool
You'll see either "role": "anon" (safe, public) or "role": "service_role" (secret, rotate now). Newer Supabase projects use clearer prefixes instead of JWTs: sb_publishable_... (public) vs sb_secret_... (secret). The prefix tells you everything — no decoding needed.
Rotating each secret
The Supabase keys
In the dashboard, go to Project Settings → API Keys. How rotation works depends on which key system your project uses:
- New keys (sb_publishable_ / sb_secret_): these are independent of each other and of the JWT secret. Revoke the leaked sb_secret_ key and create a new one; the publishable key is unaffected because it wasn't a secret.
- Legacy JWT keys: both the anon and service_role keys are JWTs signed by your project's JWT secret. Rolling that secret regenerates both keys at once, because both are signatures over it.
Either way, after rotating the secret:
1. Copy the new service_role / sb_secret_ key into your server environment (Vercel project env vars, .env.local, etc.).
2. If you rolled a legacy JWT secret, also grab the regenerated anon key and redeploy so the fresh key ships in your bundle.
One consequence to expect on the legacy path: rolling the JWT secret invalidates every JWT signed with the old one, which logs out all existing user sessions. That's the correct, safe outcome after a leak — do it anyway.
Database password
Project Settings → Database → Reset database password. This changes the password embedded in your DATABASE_URL / connection string. Update that string everywhere it lives (Vercel, CI secrets, local .env.local, migration tooling). Anything still using the old password starts failing — that's your checklist of places to fix.
Third-party keys
Each provider has its own flow, but the pattern is identical: generate a new key, deploy it, then revoke the old one. Don't skip the revoke — a new key does not disable the leaked one.
- Stripe: Developers → API keys → roll the leaked secret key.
- Resend / OpenAI / etc.: create new key, swap in env, delete old.
Now purge git history
Rotation makes the leaked values worthless. Purging history is about hygiene and not re-leaking on the next clone. Deleting the file in a new commit is not enough — the old commit still contains it.
The clean tool is git filter-repo:
pip install git-filter-repo
# remove the file from all of history
git filter-repo --path .env --invert-paths
Or with BFG:
bfg --delete-files .env
git reflog expire --expire=now --all && git gc --prune=now --aggressive
Then force-push the rewritten history:
git push origin --force --all
git push origin --force --tags
Two honest caveats: force-pushing rewrites history for every collaborator (they'll need to re-clone or reset), and GitHub caches commits — forks and cached commit views can retain the blob even after your push. That's exactly why rotation is the real fix and history-purging is secondary. Then add .env to .gitignore so this can't recur:
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
git rm --cached .env # stop tracking without deleting your local copy
The part everyone skips: is your RLS actually protecting you?
Here's the uncomfortable truth. If a leaked anon key genuinely exposes your data, the anon key wasn't the vulnerability — your RLS was. That key is public to every visitor regardless of any git leak.
A few things worth checking on every table:
- RLS is enabled on the table. Enabling it with no policies denies all access through the API — that's fail-safe, not broken.
- A role needs both a matching policy and the underlying table GRANT.
- A policy with no TO clause applies to every role, including anon.
- USING (true) on a SELECT policy makes the table world-readable. It's only world-writable if a permissive INSERT/UPDATE/ALL policy also exists (INSERT policies gate rows with WITH CHECK, not USING).
- For an anonymous request, auth.uid() is NULL — so USING (auth.uid() = user_id) correctly returns nothing to anon.
Quick audit of what's exposed:
-- tables with RLS OFF (guarded only by GRANTs, no policy gate)
select relname
from pg_class
where relkind = 'r'
and relnamespace = 'public'::regnamespace
and not relrowsecurity;
-- every policy, so you can eyeball no-TO and USING(true)
select schemaname, tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename;
If reading those results makes you nervous, I put together a free, read-only repro and audit kit here: cekuu35/supabase-rls-leak-demo. It has a deliberately leaky schema plus copy-paste SQL that flags world-readable tables and no-TO policies, so you can see exactly what an anon key can reach in your own project.
The 60-second recap
1. Don't panic, don't just delete the file.
2. Anon key + project URL + NEXT_PUBLIC_ vars: safe, no rotation needed.
3. service_role, JWT secret, DB password, third-party keys: rotate now, revoke the old ones.
4. Purge history with filter-repo/BFG and force-push — but only after rotating.
5. Audit your RLS, because that's what actually stands between the public anon key and your data.
If you want to go deeper than the free audit — a full policy-by-policy checklist, the common false-positive traps, and fix templates — I keep a $29 RLS Audit Kit that packages it all up. Totally optional; the free demo repo above is enough to check and fix most projects on your own. Either way: rotate first, breathe second, and get RLS right so the next leak is a shrug instead of a scramble.
過去にコミットした.envはgitの履歴から読める:見つけ方と git-filter-repo での消し方(git 2.47で確認)
結論
- .gitignore に足して git rm --cached .env しても、1つ前のコミットからは git show HEAD~1:.env で中身が出ました(筆者の環境で確認)。
- 履歴に残っているかは git log --all --full-history -- '.env*' '**/.env*' などで探せます。消すときは git filter-repo --sensitive-data-removal --invert-paths --path .env を、新しく取ったクローンで実行します(GitHub公式の手順)。
- 履歴を書き換えても、漏れた鍵は無効になりません。最初にやるのは鍵の再発行(ローテーション)です(GitHub公式)。書き換え前のクローンには、消したはずの .env がそのまま残っていました(筆者の環境で確認)。
> この記事は「AIエージェントと秘密情報」の検証シリーズです。新しい検証は X(@supportapp3_lab)でもお知らせしています。
この記事は、AIエージェントと秘密情報の話の続きです。Codex CLIに.envを読ませない設定 では、.env の読み取りを deny しても git show HEAD:.env で中身が出ることを確かめました。deny は「いま作業ツリーにあるファイル」を守る設定で、git の中に残った同じ内容までは守りません。今回はその穴を、履歴の側から塞ぎます。
検証環境と範囲
- Debian 13 / git 2.47.3 / git-filter-repo 2.47.0(pip で一時的な venv に導入)
- GitHub ではなく、ローカルの bare リポジトリを origin に見立てて確かめました。
- 置いた値はすべてダミーです(DUMMY_API_KEY=dummy_not_a_secret_123 など)。検証後に削除しました。
確かめていないこと
- GitHub 上での挙動(キャッシュされた表示、PR 参照、フォーク)。この部分は GitHub 公式ドキュメントの記載だけを紹介します。
- macOS と Windows での挙動。
- Claude Code で Read(./.env) を deny したときに git show HEAD~1:.env が止まるか(後述)。
1. 「消したつもり」の状態を作る
.env と config/.env.local をコミットしてから、よくある手順で「管理対象から外し」ました。
git add . && git commit -m "initial commit" # うっかり .env ごとコミット
git rm --cached .env config/.env.local
printf '.env\n.env.*\n' > .gitignore
git add .gitignore && git commit -m "stop tracking .env"
この状態で試した結果です。
$ git status --short --ignored
!! .env
!! config/
$ git show HEAD:.env
fatal: path '.env' exists on disk, but not in 'HEAD'
$ git show HEAD~1:.env
DUMMY_API_KEY=dummy_not_a_secret_123
最新のコミットには .env がありません。でも1つ前のコミットを指定すると、中身がそのまま出ました。push 済みなら、リモートにも同じ履歴があります。「最新のコミットに無い」は「安全」ではありません。
2. 自分のリポジトリを点検する3つのコマンド
① .env を追加・削除したコミットを探す
$ git log --all --full-history --oneline -- '.env*' '**/.env*'
74d5f5a stop tracking .env
03f2157 initial commit
--all で全ブランチとタグ、--full-history で履歴の簡略化をせずに探します。何も出なければ、その名前のファイルはコミットされていません。
② 履歴の中にある .env 系のファイルを一覧にする
$ git rev-list --all --objects | grep -E '(^| |/)\.env'
17f709d7339578e5b4b54e9d4153693ddf609837 .env
7de58cddc49b61f742d30ac9d03fb9fb8e9cff12 config/.env.local
左の値はファイルの中身(blob)のハッシュです。あとで「本当に消えたか」を確かめるときに使います。
③ 値そのものを、全コミットから探す
$ git grep -n dummy_ $(git rev-list --all)
03f2157484012a8b12d24cd07642ed29f810b0c7:.env:1:DUMMY_API_KEY=dummy_not_a_secret_123
03f2157484012a8b12d24cd07642ed29f810b0c7:config/.env.local:1:DUMMY_DB_PASS=dummy_local_456
ファイル名を変えてコミットしてしまった場合も、これなら見つかります。本物の鍵を探すときは、鍵の値そのものをコマンドに打たないでください。シェルの履歴に残ります。API_KEY= のような変数名で探すのがおすすめです(筆者の提案)。
おまけ:stash にも残る
git stash -u は ignored のファイルを含みません。筆者の環境では、変更が .env だけだったので stash 自体が作られませんでした。一方、git stash --all は ignored のファイルも含めるので、.env が stash に入りました。
$ git stash -u
No local changes to save
$ git stash --all
Saved working directory and index state WIP on main: 74d5f5a stop tracking .env
$ git show 'stash@{0}^3:.env'
DUMMY_API_KEY=dummy_changed_789
stash はローカルにしか残らず、普通の push では送られません。ただ、AIエージェントが git show で読める場所が1つ増えます。
3. 見つかったら:やる順番
① 先に鍵を再発行する
GitHub の公式ドキュメントは、秘密情報なら最初に revoke や rotate をするよう書いています。再発行すれば古い鍵はもう使えないので、それで十分なら、履歴の書き換えまではしなくてよい場合もあるとも書かれています。
履歴を書き換えても、それまでに誰かが取得した鍵は使えるままです。順番を逆にしないでください。
② 新しく取ったクローンで git-filter-repo を実行する
--sensitive-data-removal は git-filter-repo 2.47 以上で使えます(GitHub公式)。
最初に、作業していたリポジトリでそのまま実行してみました。すると、こう言われて止まりました。
$ git filter-repo --sensitive-data-removal --invert-paths --path .env --path config/.env.local
Aborting: Refusing to destructively overwrite repo history since
this does not look like a fresh clone.
(expected at most one entry in the reflog for HEAD)
Note: when cloning local repositories, you need to pass
--no-local to git clone to avoid this issue.
Please operate on a fresh clone instead. If you want to proceed
anyway, use --force.
GitHub の手順も、まずクローンしてからその中で実行する流れです。新しくクローンして実行し直しました(ローカルのリポジトリからクローンしたので --no-local を付けています)。
$ git clone -q --no-local origin.git fresh
$ cd fresh
$ git filter-repo --sensitive-data-removal --invert-paths --path .env --path config/.env.local
NOTICE: Fetching all refs from origin to make sure we rewrite
all history that may reference the sensitive data, via
git fetch -q --prune --update-head-ok --refmap "" origin +refs/*:refs/*
Parsed 2 commitsHEAD is now at db361b5 stop tracking .env
New history written in 0.03 seconds; now repacking/cleaning...
You rewrote 2 (of 2) commits.
NOTE: First Changed Commit(s) is/are:
03f2157484012a8b12d24cd07642ed29f810b0c7
NOTE: LFS object orphaning not checked (LFS not in use)
(以下、次の手順の案内が続く)
- --path には、ファイル名だけではなくリポジトリ内のパスを書きます。移動や改名をしたことがあるなら、前のパスも --path で足してください(GitHub公式)。
- First Changed Commit(s) の値は、GitHub Support に連絡するときに使います(GitHub公式)。
③ 消えたか確かめる
$ git log --all --full-history --oneline -- '.env*' '**/.env*'
$ git grep -n dummy_ $(git rev-list --all)
$ git cat-file -p 17f709d7339578e5b4b54e9d4153693ddf609837
fatal: Not a valid object name 17f709d7339578e5b4b54e9d4153693ddf609837
$ git log --oneline
db361b5 stop tracking .env
9278ca3 initial commit
探すコマンドは2つとも何も出さなくなり、2章②で控えた blob のハッシュも見つからなくなりました。コミットのハッシュは全部変わっています。
--sensitive-data-removal を付けた今回は、origin の設定が残りました(git remote -v で確認)。別の新しいクローンで、このオプションを付けずに実行したときは origin の設定が消えました。次の push の前に git remote -v を見ておくと安全です(筆者の環境で確認)。
④ force push する
$ git push --force --mirror origin
To /path/to/origin.git
+ 74d5f5a...db361b5 main -> main (forced update)
(パスは書き換えています)
GitHub では、refs/pull/ で始まる参照は読み取り専用なので、push が失敗します(GitHub公式)。ブランチ保護で force push を禁止しているなら、一時的に外す必要があります。
4. 消しても残る場所
① 書き換え前のクローン(実機で確認)
書き換え前に作っておいた別のクローンでは、旧 blob も旧コミットもそのまま読めました。
$ git cat-file -p 17f709d7339578e5b4b54e9d4153693ddf609837
DUMMY_API_KEY=dummy_not_a_secret_123
$ git show HEAD~1:.env
DUMMY_API_KEY=dummy_not_a_secret_123
GitHub 公式によると、古いクローンを持つ人が書き換え後に git pull して git push すると、秘密情報がまた戻ってきます。古いクローンは捨ててクローンし直してもらうのが確実です。
② リモート側にも、しばらく残る(実機で確認)
force push した直後の bare リポジトリでは、旧 blob がまだ残っていました。git gc --prune=now を実行して、やっと消えました。
$ git --git-dir=origin.git cat-file -t 17f709d7339578e5b4b54e9d4153693ddf609837
blob
$ git --git-dir=origin.git gc -q --prune=now
$ git --git-dir=origin.git cat-file -t 17f709d7339578e5b4b54e9d4153693ddf609837
fatal: git cat-file: could not get object info
これは自分で管理する bare リポジトリでの結果です。GitHub ではサーバー側の gc を自分では実行できません。
③ GitHub 上(公式の記載のみ。実機では未確認)
GitHub 公式によると、書き換えて force push したあとも、次の場所からは読めます。
- クローンとフォーク
- SHA を直接指定した、GitHub 上のキャッシュされた表示
- そのコミットを参照している PR
キャッシュされた表示と PR の参照は、GitHub Support に頼めば消してもらえます。ただし、鍵のローテーションでリスクを抑えられる場合は対応しないと明記されています。フォークに残ったものは、フォークの持ち主に頼むしかありません。
5. AIエージェントの deny では止まらない理由
Claude Code の公式ドキュメントでは、Read の deny が効く範囲を次のように説明しています。
- 組み込みのファイルツールと、Bash で認識できるファイルコマンド(cat、head、tail、sed、tee)、リダイレクトの対象には効く。
- ファイル名を指定せずに読むコマンドや、スクリプトのように間接的にファイルを読むサブプロセスには効かない。OS レベルで止めたいなら sandbox を使う。
- git の読み取り系のコマンドは、確認なしで実行される組み込みの読み取り専用コマンドに含まれる。
git show HEAD~1:.env は、作業ツリーの .env ではなく .git の中のオブジェクトを読みます。なので Claude Code で Read(./.env) を deny していても、止まらない可能性があります。ただし、これは公式の記載から考えた推測で、実機では確かめていません。 Codex CLI では、deny を設定しても git show HEAD:.env で読めたことを 前回の記事 で確認しています。
AI に読ませたくない鍵が履歴に一度でも入ったなら、deny の設定より先に、鍵の再発行が必要です。
6. 再発防止
GitHub 公式が挙げている方法です。
- .gitignore に .env を書き、そのファイル自体もコミットして共有する
- git add . や git commit -a を避け、ファイルを1つずつ追加する。git diff --cached で中身を見てからコミットする
- pre-commit フックで秘密情報を検出する(git-secrets や gitleaks など)
- GitHub の push protection を有効にする
AIエージェントの側では、次の設定も合わせて行ってください。
- Claude Code: settings.json の permissions.deny
- Codex CLI: permission profile の deny
- どのエージェントが .env を読めそうかの点検: ファイル名だけで点検する日本語CLI
まとめ
- git rm --cached と .gitignore だけでは、過去のコミットに .env が残る(筆者の環境で確認)。
- git log --all --full-history、git rev-list --all --objects、全コミットへの git grep で探せる。
- 見つかったら、まず鍵を再発行する。そのうえで必要なら、新しく取ったクローンで git filter-repo --sensitive-data-removal を実行し、force push する。古いクローン、フォーク、GitHub 上のキャッシュには残る(クローンは実機、それ以外は公式)。
関連記事:
- Codex CLIに.envを読ませない設定:公式例の "**/*.env" だけでは .env.local と深い階層が読めた
- Claude Codeに.envを読ませない設定は settings.json の permissions.deny に書く
- Claude Codeの会話記録は ~/.claude/projects/ に平文で30日残る
- AIエージェントに.envを読まれていないか、ファイル名だけで点検する日本語CLIを作った
参考(公式):
- Removing sensitive data from a repository – GitHub Docs
- git-filter-repo(newren/git-filter-repo)
- git-stash ドキュメント
- Configure permissions – Claude Code Docs
※ 仕様は 2026-10-19 時点の公式ドキュメントに基づきます。検証は git 2.47.3 / git-filter-repo 2.47.0 で、Linux の使い捨て環境でのみ行いました。
試してみる
この記事の①の点検をまとめて行うCLIを公開しました(MIT License): supportapp3-lab/ai-secret-check
npx github:supportapp3-lab/ai-secret-check scan-history ./調べたいリポジトリ
全ブランチ・stash を含む履歴から、どの階層の .env / .env.* が追加されたかを、コミットの短いハッシュと件数だけ表示します。ファイルの中身は取り出しません。見つかったら、まず鍵の再発行から始めてください。
更新情報やちょっとしたTipsは X(@supportapp3_lab)で発信しています。
An AI Coding Agent Uploaded Whole Git Histories: How to Lock Down Your Homelab
On 18 September 2026 a developer called ferstar published a teardown of ZCode, the desktop coding app from Z.ai (Zhipu AI) built on the GLM models, and the Hacker News thread that followed put it in front of everyone. While clearing disk space, ferstar noticed ~/.zcode holding more than 700 MB, unpacked the app, and found that a logged in client was packaging workspace snapshots and uploading them to Alibaba Cloud object storage. The snapshots were not just the source tree: in one 313 MB example covering 42,411 files, 86.6 percent of the payload was the .git directory, LFS objects and reflogs included. Each snapshot was encrypted with a key wrapped to a server held certificate, so the user could not decrypt their own upload. AI coding tools that run locally or integrate into your editor have deep filesystem access by design, and that access is increasingly being used in ways that are not obvious from the UI. If you run a homelab and use any AI agent that touches your code, this is your wake-up call.
The resolution first, because it changes how you should read the rest. Z.ai responded the same day, initially attributing the uploads to a wiki generation feature. It deleted the Alibaba Cloud bucket on 20 September and on 21 September published ZCode's source under Apache 2.0 at zai-org/ZCode, citing third party audits. ferstar's follow-up review of the published code found the upload endpoint, the object storage path and the key delivery code all absent from version 3.14.0 onward, with indexing running on locally bundled tools. So the specific behavior is gone. What nobody can audit is what happened to whatever was uploaded before 18 September, which is exactly why the cleanup below is still worth doing. Now: why homelabbers are a soft target, and what to do about it.
💡 Why This Matters for Homelabbers
Your homelab repos are not just toy projects. They often contain .env files, hardcoded API keys, internal IP ranges, VPN configs, Ansible inventories, and Docker Compose files with credentials baked in. Git history makes this worse because a secret you deleted three commits ago is still fully recoverable from the log.
AI coding agents are designed to read your project context, and that context is your entire repo tree. When an agent "understands your codebase," it is reading files and history. The question is whether it keeps that data local or phones home with it.
🧰 Prerequisites
- A Linux or macOS machine running AI coding tools (ZCode, Cursor, the GitHub Copilot coding agent, Cline, Continue, etc.). If you see GitHub Copilot Workspace referenced in older guides, that preview was sunset on 30 May 2025 and its ideas moved into the Copilot coding agent.
- Basic comfort with the terminal and Git
- Optional: a network monitoring tool like mitmproxy, Wireshark, or a Pi-hole with logging enabled
🔍 Step 1: Audit What Your AI Tools Are Actually Sending
Before you lock anything down, you need to know what is leaving your machine. The bluntest tool here is watching outbound connections while your AI agent is active.
On Linux, use ss or nethogs to watch live traffic per process:
# Watch outbound connections from a specific process name
ss -tp | grep zcode
# Or monitor per-process bandwidth in real time
sudo nethogs
If you want to inspect the actual payload, route the tool's traffic through mitmproxy. Install it, set your HTTP_PROXY and HTTPS_PROXY environment variables to point at it, and trust its certificate. Then launch the AI agent from the same shell session and watch what it posts. Check the official docs for your specific tool to understand whether it respects proxy settings.
export HTTP_PROXY=http://127.0.0.1:8080
export HTTPS_PROXY=http://127.0.0.1:8080
# Then launch your agent in the same terminal
🔧 Step 2: Scrub Secrets from Git History Before They Can Be Harvested
If you have ever committed a secret to a repo that an AI agent can read, treat that secret as compromised. Rotate it first, then clean the history. The two most practical tools for this are git filter-repo and BFG Repo Cleaner. Check each project's documentation for the exact flags, since the syntax differs and getting it wrong can corrupt history.
A basic git filter-repo invocation to strip a file from all history looks like this:
# Install git-filter-repo first (see https://github.com/newren/git-filter-repo)
git filter-repo --path secrets.env --invert-paths
After rewriting history, force-push to all remotes and immediately rotate any exposed credentials. Do not skip the rotation step. Cleaning history only helps for future reads.
🛡️ Step 3: Use a .gitignore (and a Global One)
The fastest mitigation is making sure sensitive files never enter Git in the first place. Add a thorough .gitignore to every project and also configure a global gitignore that covers your whole machine.
# Set a global gitignore
git config --global core.excludesFile ~/.gitignore_global
# ~/.gitignore_global: common patterns to exclude
.env
.env.*
*.pem
*.key
id_rsa
id_ed25519
secrets/
vault_pass.txt
ansible/group_vars/all/vault.yml
This does not protect history that already exists, but it prevents new secrets from being committed accidentally, which is the most common source of leaks.
🛡️ Step 4: Sandbox AI Agents with Network Policies
If you want a harder boundary, run AI coding tools inside a sandboxed environment that limits outbound network access. There are a few practical ways to do this in a homelab.
- Docker with network restrictions: Run the agent in a container with --network none or a custom bridge that only allows connections to your LAN and a specific AI API endpoint.
- Firewall rules per user: On Linux, you can use iptables or nftables to restrict outbound traffic from a specific Unix user that you create just for running the agent.
- A dedicated VLAN: If you have managed switches and a router that supports VLANs (like pfSense or OPNsense), put your dev machine on a VLAN where you can log and filter all outbound connections at the firewall level.
The VLAN approach is the most powerful because it works regardless of what the agent does inside your OS. You see everything at the network edge.
🛡️ Step 5: Prefer Local or Self-Hosted Models Where Possible
The cleanest solution to data exfiltration is keeping data on your own hardware. Tools like Ollama let you run capable open-source models locally and expose them on a local API endpoint. Many AI coding frontends (Continue, for example) can be pointed at a local Ollama instance instead of a cloud API.
# Example: point Continue or a compatible tool at a local Ollama endpoint
# In the tool's config, set the API base URL to:
http://localhost:11434
You give up some model capability compared to frontier cloud models, but for sensitive internal codebases the trade-off is often worth it. Check the documentation for whichever coding agent you use to confirm it supports custom API base URLs and what model format it expects.
🔧 Troubleshooting
- mitmproxy shows no traffic from the agent: Some tools use certificate pinning or ignore system proxy settings. Try monitoring at the network level with Wireshark on your loopback or default interface instead.
- git filter-repo says "not a git repository": Make sure you are running the command from inside the repo root, and that you have the tool installed correctly. It is not bundled with Git by default.
- Ollama model responses are slow: This is normal on CPU-only hardware. Quantized models (Q4 or Q5) give you a better speed and quality balance on machines without a dedicated GPU. Check the Ollama model library for available quantization levels.
✅ Wrap Up
The ZCode incident is a reminder that AI coding agents are powerful precisely because they have broad access to your project context, and that access can be abused, intentionally or accidentally. ZCode itself was patched and opened up within days, so treat this as a warning about the category rather than about one product, and note what it took to find out: a paying user had to measure a directory, unpack an Electron bundle and watch the traffic. Your homelab repos deserve the same scrutiny you would give a production environment.
Start by auditing what your current tools are sending, scrub any secrets from Git history, and put network-level controls in place so you can see and limit outbound connections. If you are dealing with genuinely sensitive internal code, a local model via Ollama is worth the setup time. These are not exotic measures. They are just good hygiene for an era where your code editor is also a networked agent.
---
Related from the lab
- Better Homelab Secret Management with Infisical
- An AI Agent Ran Up a $6,531 AWS Bill Scanning DN42. Here's How to Cap Yours
- CrowdSec: Community-Powered Security
---
Originally published at codewithromi.com.
The First Cut: How Module Boundaries Get Drawn and Why They Drift
Part 2 of the Production Codebase series. New here? Start with Part 1.
Software Architecture from First Principles — Part 2: Monolithic Entanglement, Coupling Mathematics, and Language-Enforced Boundaries
---
The Thirty-Second Test
Clone a healthy production repository, open the root directory in your editor, and inspect the file tree. Within thirty seconds, without inspecting a single function or reading a single implementation file, the operational reality of the system reveals itself. You know where domain business logic lives, where tests are segregated, which team owns the payment processor, how to spin up dependencies locally, which environment variables are mandatory, and how database migrations are sequenced. You have not sent a single message on Slack. You have not interrupted a tech lead. The physical structure of the repository answered your questions before you had to ask them.
In the first article, we established that folder structure is a system design decision — shaped by Conway's Law, forcing functions, and team communication patterns. We introduced the fintech engine and promised to open it. This is that article.
Now open the other kind of repository. Forty-three files sit strewn across the top-level directory. You see FEATURE_240_DESCRIPTION.md, analytics.ts, fin.java, and IMPLEMENTATION_COMPLETE_SUMMARY.md mixed directly alongside package.json and tsconfig.json. Three separate docker-compose files exist with names like docker-compose.local.yml, docker-compose.dev.backup.yml, and docker-compose.final.yml, none of which document which one actually works. The .env.example file contains stale database credentials from four years ago that reference a Postgres instance that was migrated to Amazon Aurora during the pandemic. You do not begin coding. You close your terminal, open Slack, and ask who knows how to run this project.
The difference between those two repositories has nothing to do with the algorithmic brilliance of the code inside them. It has everything to do with whether the engineers who built the system treated the root directory as an intentional architectural contract or as an unmonitored staging ground for sprint artifacts.
fintech-engine/
├── .github/ # CI workflows and templates
│ ├── workflows/ # Validation and deployment pipelines
│ └── pull_request_template.md
├── .gitignore # Source control boundary
├── .dockerignore # Build context filter
├── .env.example # Local execution contract
├── CODEOWNERS # Ownership and compliance control
├── LICENSE
├── README.md # Entry point and setup guide
├── CHANGELOG.md # Human-readable change history
├── justfile # Task runner interface
├── docker-compose.yml # Local dependency topology
├── docs/
│ ├── adr/ # Immutable decision records
│ ├── architecture/ # System maps and diagrams
│ ├── onboarding/ # New engineer ramp-up guides
│ └── runbooks/ # Incident playbooks
├── infra/
│ ├── terraform/ # Cloud resource provisioning
│ └── k8s/ # Container scheduling manifests
├── migrations/ # Immutable schema scripts
├── scripts/
│ ├── local/ # Workstation scripts
│ └── ci/ # Pipeline scripts
├── src/ # Application source code
└── tests/
├── unit/ # Fast in-memory tests
├── integration/ # Database and broker tests
└── e2e/ # End-to-end journey tests
---
The Locational Predictability Imperative
There is an unspoken assumption among junior developers that good engineers read every line of code in the systems they maintain. In a real production system containing one hundred thousand or five hundred thousand lines of code, nobody reads every line of code. Tech leads, staff architects, and on-call responders survive by reading almost none of it. They navigate systems by forming coarse mental maps of ownership, boundaries, and lifecycles, diving into precise implementation lines only when an incident strikes or a specific interface requires extension.
Empirical software engineering research confirms this reality. In their landmark study on developer activity, I Know What You Did Last Summer: An Investigation on How Developers Spend Their Time, researchers Roberto Minelli, Andrea Mocci, and Michele Lanza tracked developers across professional tasks and discovered that programmers spend 58 to 70 percent of their working time comprehending, navigating, and inspecting codebases, while spending only 5 percent of their time actively editing or typing code. Decades earlier, Robert C. Martin observed in Clean Code that the ratio of time spent reading code versus writing code exceeds ten to one.
When a junior engineer submits a pull request introducing a new merchant discount rule, a senior reviewer should not have to execute a full-text search across fifty directories to determine where the logic was placed. A well-designed codebase exhibits locational predictability: given a business domain concept or a bug description, any experienced engineer should be able to deduce the exact directory, file, and interface where the modification belongs before opening the file tree. When a codebase lacks locational predictability, every code review becomes an archaeological expedition, and the seventy-percent navigation tax compounds into organizational paralysis.
---
The Physical Geography of Repository Contracts
The root of a production repository houses the physical contracts governing how code is built, tested, and audited, beginning with src/ to isolate application domain logic from development scaffolding. Yet treating src/ as a universal convention overlooks other language ecosystems. In Go repositories, binaries live in cmd/ while internal libraries reside in internal/. Introduced by Russ Cox in Go 1.4, any package inside an internal/ directory is mechanically restricted by the compiler: packages outside the immediate parent hierarchy cannot import it. While teams working in TypeScript or Python frequently rely on fragile lint rules that erode under sprint panic, Go turns directory placement into a compiler-enforced boundary.
Database transformations inside migrations/ operate under a single non-negotiable law: applied migrations are immutable. Once a schema migration script is merged to the main branch and executed against any shared environment, that file must never be modified. Migration frameworks like Flyway enforce this discipline by calculating cryptographic checksums of every script and verifying them against a history ledger table, aborting deployments if a single byte has changed. To manage zero-downtime schema evolution safely across rolling releases without table-locking outages, teams apply the expand-contract pattern formalized by Pramod Sadalage and Martin Fowler, decoupling schema expansions, dual-writing code deployments, data backfills, and contracting column removals into distinct, non-breaking phases.
Repository governance centers around CODEOWNERS, which performs three simultaneous architectural jobs. First, it routes pull requests to designated domain experts based on path matching. Second, it acts as a regulatory compliance control for Segregation of Duties (SoD) under Section 404 of the Sarbanes-Oxley Act (SOX) and SOC 2 Type II Common Criteria CC6.1, mathematically preventing authors from self-approving changes to sensitive financial calculation or authorization paths. Third, it serves as an automated blast-radius audit: any directory lacking an explicit code owner represents an unmonitored path that can bypass mandatory security and architectural reviews.
The defensive perimeter of the repository concludes with .env.example, .gitignore, and .dockerignore. The .env.example file is an executable security contract that defines mandatory environment keys, credential origins, and safe local defaults without exposing live secrets. Behind it, .gitignore protects against accidental secret leakage into source control—where committing a credential requires immediate rotation and history rewriting with git-filter-repo, since git packfiles retain deleted files indefinitely. Alongside it, .dockerignore prevents build daemons from leaking local environment files into distributable image layers, backed by pre-commit scanners like TruffleHog or Gitleaks to intercept credentials before they enter git history.
---
The AI Code Generation Paradox
This discipline matters more in 2026 than it ever has — because the next reader of your repository root is not always human. In 2026, the marginal cost of writing code has effectively dropped to zero. Any developer or autonomous agent can invoke a modern language model and generate five hundred lines of syntactically flawless implementation in seconds. Teams celebrate the illusion of velocity because tickets move across sprint boards faster than ever before.
Yet software engineering has never been governed by the speed of syntax generation; it has always been governed by the economics of maintenance. A comprehensive empirical investigation, Coding on Copilot: 2023 Data Shows Downward Pressure on Code Quality by GitClear, analyzed 153 million lines of code written between 2020 and 2023. Their research revealed that code churn projected to double compared to pre-AI baselines. Simultaneously, the proportion of copy-pasted code rose dramatically, while deliberate refactoring, code movement, and deduplication plummeted.
The mechanism driving this trend is structural. Large language models operate within finite context windows and optimize for local completion rather than global coherence. When an AI assistant or autonomous coding agent is asked to implement a feature in a sprawling repository with ambiguous directory boundaries, it suffers from context poisoning and prompt pollution: scanning the repository root ingests stale handoff documents and conflicting configuration artifacts into the agent's working memory, which the model treats as authoritative ground truth. Rather than constructing a unified domain abstraction, the agent takes the path of least resistance: it duplicates utility helpers, introduces direct cross-module couplings, or dumps ephemeral scripts into the root folder. Without rigid physical boundaries and explicit architectural constraints, AI coding assistants do not eliminate technical debt; they accelerate repository decay at ten times human speed. In the AI era, architecture is no longer merely a human convention. It is the structural fence that prevents automated agents from drowning a production system in unmaintainable sludge.
---
The Morning After the Folder Reorganization
Now assume your team executed this root discipline flawlessly. You cleaned up root sprawl, established immutable migrations, standardized workstation automation with just, decoupled deployment manifests per ArgoCD guidelines for a separate config repo, and created crisp domain directories inside src/: /auth/, /transactions/, /ledger/, and /webhooks/. The pull request merged, and the team celebrated.
Then, three weeks later, a critical merchant integration deadline loomed at 4:30 PM on a Thursday. A senior engineer needed to verify whether a customer had an active KYC flag before allowing a webhook retry. The KYC verification lived in /auth/. The webhook retry logic lived in /webhooks/. Under deadline pressure, the engineer did not design an asynchronous domain event or an anti-corruption interface. They simply typed:
import { verifyCustomerKYC } from '../auth/service';
The pull request passed code review because the reviewer was rushing to cut the sprint release. It passed continuous integration because the TypeScript compiler resolved the path without issue. With a single keystroke, the architectural boundary was punctured. Six months later, /auth/ imported /transactions/, /transactions/ imported /ledger/, /ledger/ imported /webhooks/, and /webhooks/ imported /auth/. On disk, the repository appeared neatly partitioned into four domain folders. In memory, the application had collapsed into a tangled, circular distributed hairball.
!Diagram 10: Week 1 Intended Decoupling vs. Month 6 Tangled Hairball
This represents the central operational dilemma of software architecture: drawing boundaries on a whiteboard is trivial, but keeping them from eroding under sprint pressure is where engineering actually occurs.
---
The Running Case Study: The Entangled Fintech Engine
To observe how boundaries erode and how to restore them, consider a production fintech payment engine. Financial systems operate under strict regulatory and mathematical constraints, making them ideal for studying boundary decay.
fintech-engine/
└── src/
├── auth/
│ ├── token.ts # Directly queries transaction tables for fraud flags
│ └── user.ts
├── transactions/
│ ├── processor.ts # Imports ledger directly; bypasses immutability invariants
│ └── gateway.ts # Talks to Stripe/Adyen
├── ledger/
│ ├── entry.ts # Imports webhook dispatcher inside atomic transactions
│ └── account.ts
├── webhooks/
│ ├── dispatcher.ts # Synchronously invoked by ledger; network timeouts crash payments
│ └── payload.ts
└── shared/
└── db.ts # Shared global connection pool exposing raw SQL execution
!Diagram 11: Entangled Dependencies and Shared Database Pool in the Fintech Engine
This system must protect an inviolable architectural invariant: the double-entry balance invariant. Across all ledger accounts, the sum of all debits must strictly equal the sum of all credits at all times. No external service—whether an authentication handler, an external payment gateway adaptor, or a webhook dispatcher—may ever directly write to, bypass, or mutate ledger balance records.
In the unsegregated codebase, that invariant is compromised daily. Novice engineering teams routinely treat account balances as mutable state, issuing destructive updates like UPDATE accounts SET balance = balance + 100. In high-throughput environments, this design pattern guarantees row lock contention, deadlocks, and silent reconciliation discrepancies. In contrast, industry benchmark financial platforms—such as Stripe's immutable ledger infrastructure handling five billion events per day—treat money movement strictly as an append-only event stream of balanced debit and credit entries. Balances are derived projections rather than mutable database columns, completely decoupling the financial source of truth from transient application state.
The first time I saw this pattern in production, the circular dependency wasn't obvious from the code — it was obvious from the deployment. Changing the ledger required redeploying the auth service, which required redeploying the webhook dispatcher. Nobody could explain why anymore.
In our entangled fintech engine, when a payment arrives, processor.ts opens a database transaction, calls gateway.ts to charge a credit card, directly executes an SQL update against the ledger accounts table, and immediately invokes dispatcher.ts to send a webhook to the merchant. If the merchant's webhook endpoint times out or returns a 500 error, the entire database transaction rolls back, undoing the ledger entry even though the customer's credit card was already charged. The business logic of the ledger was held hostage by an unreliable external network call because no architectural boundary isolated their failure domains.
---
The Mathematics of Boundaries: Coupling Metrics That End Subjective Debates
Architectural reviews frequently devolve into subjective debates where senior engineers argue personal preferences regarding clean code. To eliminate subjectivity, architects rely on the package coupling metrics formalized by Robert C. Martin.
!Diagram 12: Afferent Coupling (Ca) vs. Efferent Coupling (Ce) in the Fintech Engine
Afferent coupling, denoted as $C_a$, measures the number of classes or modules outside a package that depend upon classes inside it. A high $C_a$ indicates high responsibility: many parts of the application rely on this module, meaning any breaking changes to its public interface will reverberate across the system. Efferent coupling, denoted as $C_e$, measures the number of classes outside a package that classes inside this package depend upon. A high $C_e$ indicates high dependency: the module relies on numerous external abstractions and is vulnerable to breaking whenever its dependencies change.
From these two metrics, Robert C. Martin's instability metric detailed in Clean Architecture derives the Instability Metric, denoted as $I$:
$$I = \frac{C_e}{C_a + C_e}$$
The Instability metric ranges from zero to one. When $I = 0$, the module is maximally stable: it has zero outgoing dependencies ($C_e = 0$) and many incoming dependencies ($C_a > 0$), making it difficult and expensive to change because many systems rely on it. When $I = 1$, the module is maximally unstable: it has zero incoming dependencies ($C_a = 0$) and multiple outgoing dependencies ($C_e > 0$), making it volatile, easy to change, and dependent on external stability.
Applying this formula to our fintech engine reveals exactly where boundaries must be drawn. The ledger/core domain should be designed with $I \approx 0$: nothing in the ledger should depend on authentication, payment gateways, or webhooks. Conversely, webhooks/dispatcher should operate with $I \approx 1$: it exists at the volatile edge of the system, consuming domain events and orchestrating outbound HTTP requests.
This leads directly to the Stable Dependencies Principle (SDP): depend in the direction of stability. A module should only depend on modules that are more stable than itself. If a stable module like ledger with $I = 0.1$ imports an unstable module like webhooks with $I = 0.9$, the instability of the webhook package infects the ledger, destroying its stability and introducing unpredictable regression cascades.
!Diagram 13: Robert C. Martin's Instability vs. Abstractness Health Map and The Main Sequence
When a module is maximally stable ($I = 0$), it must also be abstract; otherwise, it becomes rigid and unmaintainable. Conversely, modules that are highly concrete should remain unstable ($I = 1$), allowing them to be modified rapidly without breaking external consumers. Calculating the distance from the Main Sequence, defined as $D = |A + I - 1|$ where $A$ represents abstractness, provides an objective mathematical index of architectural health that eliminates opinion from pull request reviews.
---
Making Boundaries Irreversible: Language-Level and Tooling Enforcement
Conventions documented in internal wikis or discussed during architecture offsites will always collapse under the pressure of production deadlines. If the compiler, the language runtime, or the continuous integration pipeline allows an illegal cross-boundary import, an engineer working under sprint duress will eventually merge it. Sustainable architecture requires mechanical enforcement.
A landmark demonstration of mechanical boundary enforcement occurred at Shopify between 2019 and 2022. Shopify operated a massive Ruby on Rails monolith containing over 2.8 million lines of code. Over a decade of hypergrowth, coupling between disparate parts of the monolith became so severe that test suites took hours to run, boot times crippled developer workstations, and unexpected regressions occurred continuously. Instead of paying the enormous operational tax of splitting into dozens of microservices, Shopify built and open-sourced Packwerk in September 2020.
Packwerk runs static analysis on Ruby abstract syntax trees during CI builds, enforcing two boundaries separately: package privacy (preventing external packages from referencing internal constants directly) and explicit dependency declarations (forbidding undeclared inter-package couplings). In their honest retrospective, Shopify acknowledged that Packwerk is "a sharp knife"—at points they even evaluated removing it due to developer friction and ongoing maintenance overhead. Yet the structural value was undeniable:
> "Packwerk has provided value in holding the line against new dependencies at the base layer of our application."
By turning module boundaries into automated build gates, Shopify significantly reduced unexpected regressions, cut test suite times, and maintained the operational simplicity of a single deployable application — without the coordination overhead of splitting into dozens of microservices.
Modern ecosystems provide distinct mechanisms to make boundaries irreversible:
!Diagram 14: The Mechanical Boundary Enforcement Hierarchy — From Compiler Gates to Linters
In the Go toolchain, internal/ packages enforce privacy directly through the compiler, terminating builds if external packages attempt unauthorized imports. In Rust, pub(crate) and hierarchical module scoping provide compiler-level guarantees that private domain logic cannot be accessed beyond designated crate or module boundaries.
In the Node.js and TypeScript ecosystem, modern packages utilize the exports map in package.json alongside subpath imports to declare public entry points while marking internal implementations unresolvable to external consumers. Furthermore, monorepos running Nx, Turborepo, or TypeScript Project References use tools like eslint-plugin-boundaries to enforce boundary rules in CI, failing builds if presentation layers import persistence databases directly.
In the Java and Kotlin ecosystems, architects achieve equivalent mechanical enforcement using ArchUnit. ArchUnit allows engineers to write unit tests that inspect compiled Java bytecode, verifying architectural rules in compiled bytecode as automated assertions:
@Test
public void ledgerShouldNotDependOnWebhooks() {
noClasses()
.that().resideInAPackage("..ledger..")
.should().dependOnClassesThat().resideInAPackage("..webhooks..")
.check(importedClasses);
}
If a developer introduces an illegal import between ledger and webhooks, the build breaks during standard unit testing, preventing the violation from ever reaching a shared branch. Stripe implemented a parallel pattern across their monolithic Ruby codebases, developing the Sorbet static type checker to enforce strict directional dependency graphs and prevent payment processing logic from coupling to customer billing models.
These automated boundary checks represent concrete implementations of Architectural Fitness Functions, an engineering discipline formalized by Neal Ford, Rebecca Parsons, and Patrick Kua in Building Evolutionary Architectures. Just as unit test suites continuously verify that business calculations produce correct numerical output, architectural fitness functions provide continuous, automated verification that structural invariants—such as package instability, directional dependencies, and blast-radius isolation—do not degrade under deadline pressure or unvetted AI-generated PR volume.
---
The Architecture of Intent
Before an engineer ever traces an execution thread through a database transaction or measures the throughput of an asynchronous message queue, the architecture of the system has already communicated its intent. The macroscopic geography of the repository speaks through its root contracts: its immutable migration ledgers, its isolated infrastructure blast radii, its automated task runners, its regulatory code ownership, and its defensive pre-commit firewalls.
When you step inside src/, that same architectural discipline transforms into boundary enforcement. By replacing subjective debates with mathematical coupling metrics, identifying core domain invariants like the double-entry balance rule, and backing those boundaries with compiler and CI gates, teams insulate their systems against sprint fatigue, deadline shortcuts, and unchecked AI-generated code sprawl.
The boundary is drawn. The enforcement is in place. But boundaries are not static — they are stress-tested every time the team grows. In Part 3, we map the universal root of the fintech engine — every folder, every governance file, every security contract — and the precise moment each one became necessary. Before we watch the system grow, we need to understand exactly what we built.
1 reaction
Mentions
【週末お掃除】git に溜まった不要な操作履歴やデータを削除してストレージ容量を節約しようと試行錯誤した記録 これなに 昨今 AI によって git 管理の在り方自体見直され始めていますが、まだまだみなさん使っているであろう git について。 .git には過去の履歴や不要データがたくさん蓄積していき、気付けばかなり肥大化していてストレージを圧迫していることがあります。 自分のマシンは以下のような感じでかなり肥大化していてしんどいのでお掃除しようと思ったので、その作業記録を個人のメモとしてまとめました。 個人用のメモなのでいろいろ不足していたりすることもありますが、必要な人は参考にしていただけたらと思います。 対象読者 .git によってストレージが食われすぎていてしんどい自分のようなストレージにお金かけられない人たち 実行環境 macOS 27 Golden Gate 27.0.1 ⚠️ 注意 後述の手順を実行すると、完全に「元に戻せない(Undoできない)」状態になります。 本来は間違えてブランチを消しても reflog から復元できますが、後述するコマンド群を実行した後はリポジトリが極限まで軽量化される代わりに、過去のミスを救済するデータもすべて消去されます。大容量ファイルの削除作業の「最後の仕上げ」としてのみ実行してください。 作業を実行する前に、現在のリポジトリのバックアップをとるなどして安全策をとるようにしてください。 先にオチだけ 結論だけ先に書くと、最終的に新しいリポジトリ爆誕させてそこにファイルだけ引っ越しました🫠(なんだそりゃ) とはいうものの、実際何をやったのかをメモしましたので笑いながら読んでください。 初期調査 なんかよくわからないけど GitHub のリポジトリを入れているところがかなり肥大化している模様でした。 タイトルですでに git が問題だということはすでにわかってはいますが、そこに行き着くまでの調査記録を念のため書きます。 ディレクトリのサイズ確認 Applications, Developer などが肥大化するのはまあわかるのですが、 Documents がかなり食っていたのは気になったので調べてみると、 GitHub のディレクトリがかなり食っていることが判明。 du コマンドで調べてみたところこんな感じでした。 % du -sh ~/GitHub/* 1.4G /Users/su3-hokkaido/GitHub/su3-app01 20M /Users/su3-hokkaido/GitHub/su3-app02 5.1G /Users/su3-hokkaido/GitHub/su3-app03 1.4G /Users/su3-hokkaido/GitHub/su3-app04 4.0K /Users/su3-hokkaido/GitHub/su3-app05 4.0K /Users/su3-hokkaido/GitHub/su3-app06 4.0K /Users/su3-hokkaido/GitHub/su3-app07 1.1G /Users/su3-hokkaido/GitHub/su3-app08 4.0K /Users/su3-hokkaido/GitHub/su3-app09 4.0K /Users/su3-hokkaido/GitHub/su3-app10 7.1G /Users/su3-hokkaido/GitHub/su3-app11 207M /Users/su3-hokkaido/GitHub/su3-app12 % ここからさらに細かい原因を調べるために、特に肥大化が激しい app11 のディレクトリを調べてみることに。 .git 肥大化が判明 さらに分解して調べてみると一つ一つは全然重くなさそう。 % du -sh ~/GitHub/su3-app11/* 1.2M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge01 804M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge02 4.0K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge03 10M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge04 76K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge05 276K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge06 3.4M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge07 28K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge08 18M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge09 148K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge10 216K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge11 60M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge12 12K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge13 1.1M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge14 % もしかして?と思って .git を調べたらビンゴ。6GBも食っていました。 % du -sh .git/objects 6.3G .git/objects % お掃除作業手順(基本編) ここからは一番大きく、かつ別に履歴は必要としていないリポジトリ(約7GB食っていたリポジトリ)に対して作業していこうと思います。 セットアップ git-filter-repo をインストールします。(インストール手順はこちら: github.com/newren/git-filter-repo/blob/main/INSTA… ) 自分は homebrew 経由でインストールしました。 % brew install git-filter-repo ==> Auto-updating Homebrew... Adjust how often this is run with $HOMEBREW_AUTO_UPDATE_SECS or disable with $HOMEBREW_NO_AUTO_UPDATE=1. Hide these hints with $HOMEBREW_NO_ENV_HINTS=1 (see man brew). ==> Downloading ghcr.io ######################################################################### 100.0% ==> Pouring portable-ruby-4.0.9.arm64_big_sur.bottle.tar.gz ==> Downloading bottle manifests ✔︎ Bottle Manifest git-filter-repo (2.47.0) Downloaded 7.3KB/ 7.3KB ==> Fetching downloads for: git-filter-repo ✔︎ Bottle git-filter-repo (2.47.0) Downloaded 101.9KB/101.9KB ==> Pouring git-filter-repo--2.47.0.all.bottle.1.tar.gz 🍺 /opt/homebrew/Cellar/git-filter-repo/2.47.0: 9 files, 361.5KB % 分析レポート作成 対象リポジトリのディレクトリで git filter-repo --analyze を実行して、お掃除前にどんなものが入っているか確認します。 実際の実行ログは以下のような形です。 % git filter-repo --analyze Processed 4009 blob sizes Processed 382 commits Writing reports to .git/filter-repo/analysis...done. % .git/filter-repo/analysis 配下に生成された各ファイルの詳細は省略しますが、ざっとのイメージは以下の通り。 詳しくは大元のリポジトリ git-filter-repo を読んだ方が確実かと思います。 path-all-sizes.txt : 全部のファイル path-deleted-sizes.txt : 過去に削除されたファイル directories-all-sizes.txt : 全部のディレクトリ directories-deleted-sizes.txt : 過去に削除されたディレクトリ 不要オブジェクトを掃除する 実行コマンドは以下の2つ。 1: 過去の操作履歴(リブログ)を今すぐ期限切れにする git reflog expire --expire=now --all Gitは、コミットの切り替え(checkout)や打ち消し(reset)、ブランチの削除など、あなたがリポジトリ内で行った「すべての操作履歴」を裏で記録しています。これを reflog と呼びます。 --expire=now: 本来なら一定期間(デフォルトで30〜90日間)保管される操作履歴を、「今すぐすべて期限切れ」にします。 --all: すべてのブランチや参照の操作履歴を対象にします。 2: 不要になったデータ実体を完全に削除し、残ったデータを強力に圧縮する git gc --prune=now --aggressive gc(ガベージコレクション)とは Git における「ゴミ集め・掃除」コマンドです。どのコミットやブランチ、操作履歴からもリンクされなくなった孤立したデータを検知して削除します。 --prune=now: 通常、直近2週間に作られたゴミデータは誤操作防止のために残されますが、それを無視して「今あるゴミを今すぐすべて完全削除」します。 --aggressive: 通常の最適化よりも、より時間をかけて強力にファイルを圧縮します。共通するコードやファイル差分を綺麗にまとめて、 .git フォルダの容量を限界まで削ります。 実際の実行ログは以下の通り。 時間の計測はしていないのですが、たぶん実行時間で言うと60分くらいかかったかも。 実行結果 % git reflog expire --expire=now --all % git gc --prune=now --aggressive Enumerating objects: 5420, done. Counting objects: 100% (5420/5420), done. Delta compression using up to 10 threads Compressing objects: 100% (5273/5273), done. Writing objects: 100% (5420/5420), done. Total 5420 (delta 2924), reused 1282 (delta 0), pack-reused 0 (from 0) % お掃除作業手順(発展編) 実行したけれども、100MB だけしか減っていなかった。。。 % du -sh .git/objects 6.2G .git/objects % 追加調査 git fsck --unreachable --dangling を実行して本当に孤立したゴミ(Dangling object)があるかを改めて確認したところ以下のようになった。 .DS_Store の話しか出てこなくて、消せそうなものが全く無し。 コマンド: git fsck --unreachable --dangling % git fsck --unreachable --dangling error: refs/.DS_Store: badRefName: invalid refname format error: refs/.DS_Store: badRefContent: Checking ref database: 100% (1/1), done. Checking object directories: 100% (256/256), done. Checking objects: 100% (5420/5420), done. Verifying commits in commit graph: 100% (382/382), done. % そこでさらに容量を食っている上位10ファイル(コミット)を特定してみます。するといろいろ素材で使っていた動画や画像のコミットが!(これだ!) コマンド: git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')" % git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')" b3d8ee51c7b74ae photos/canon/canon_eos200/IMG_5239.JPG b41cfacbd8297d5 photos/canon/canon_eos200/MVI_5303.MP4 e875256d0382801 photos/canon/canon_eos200/IMG_5322.JPG e704d9a01e0a9e2 photos/canon/canon_eos200/IMG_5323.JPG 320f19f210690ae photos/canon/canon_eos200/IMG_5324.JPG 1bb6700c6973c3a photos/canon/canon_eos200/IMG_5325.JPG d94566c2ad22e3f photos/canon/canon_eos200/IMG_5326.JPG fc3ab40e7884c53 photos/canon/canon_eos200/IMG_5327.JPG 38e68342661e8ce photos/canon/canon_eos200/MVI_5443.MP4 3ba184a31a09506 photos/canon/canon_eos200/MVI_5526.MP4 % そんなわけで画像、動画を削除しにいく --path で指定したフォルダを、歴史上から完全に除外( --invert-paths )してすべてのコミット履歴を自動で書き換えます。 冒頭でインストールした filter-repo を使って削除しに行きます。 git filter-repo --path photos/canon/canon_eos200/ --invert-paths 実行したけど破壊的な上書きだからフレッシュクローンしろと怒られました。 % git filter-repo --path photos/canon/canon_eos200/ --invert-paths Aborting: Refusing to destructively overwrite repo history since this does not look like a fresh clone. (expected at most one entry in the reflog for HEAD) Please operate on a fresh clone instead. If you want to proceed anyway, use --force. % しかし、ローカルでもう必要なものはバックアップとっているので、気にせず --force で強制実行。 % git filter-repo --path photos/canon/canon_eos200/ --invert-paths --force NOTICE: Removing 'origin' remote; see 'Why is my origin removed?' in the manual if you want to push back there. (was git@github:su3-hokkaido/su3-app11.git) Parsed 191 commits New history written in 0.21 seconds; now repacking/cleaning... Repacking your repo and cleaning out old unneeded objects HEAD is now at abc1234 hogehoge Enumerating objects: 4728, done. Counting objects: 100% (4728/4728), done. Delta compression using up to 10 threads Compressing objects: 100% (1885/1885), done. Writing objects: 100% (4728/4728), done. Total 4728 (delta 2707), reused 4722 (delta 2705), pack-reused 0 (from 0) Completely finished after 7.05 seconds. % これだけでもう 4GB くらい消えました。 Before % du -sh .git/objects 6.3G .git/objects % After % du -sh .git/objects 2.4G .git/objects % 最後にもう一度基本編で紹介したコマンドを実行して孤立データを削除しに行きます。 % git reflog expire --expire=now --all % git gc --prune=now --aggressive Enumerating objects: 4728, done. Counting objects: 100% (4728/4728), done. Delta compression using up to 10 threads Compressing objects: 100% (4590/4590), done. Writing objects: 100% (4728/4728), done. Total 4728 (delta 2700), reused 2008 (delta 0), pack-reused 0 (from 0) % が、あまり変わらず。 % du -sh .git/objects 2.4G .git/objects % git に再度 push しようにもデータ量が多すぎて、サーバーが耐えきれずに途中で 500 エラーになってしまいました。 % git remote add origin git@github.com:su3-hokkaido/su3-app11.git # filter-repo は origin を削除するので、リモートを再登録する必要あり % git push -f origin main Enumerating objects: 4728, done. Counting objects: 100% (4728/4728), done. Delta compression using up to 10 threads Compressing objects: 100% (1890/1890), done. error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500 send-pack: unexpected disconnect while reading sideband packet Writing objects: 100% (4728/4728), 2.43 GiB | 21.06 MiB/s, done. Total 4728 (delta 2700), reused 4728 (delta 2700), pack-reused 0 (from 0) fatal: the remote end hung up unexpectedly Everything up-to-date % ここから過去のコミットごとに push し直していくという作業をすれば解決する目処はついたものの、そんな気力はなく諦めました。 (スクリプト作って頑張ってもいいけど、労力的に無駄になりそう) 最終奥義 要件的には過去の reflog は全く必要なかったので「もういいや!」となって新しいリポジトリを爆誕させて必要なファイルだけそちらにお引越し。 最初からこっちでよかったね!ハハハハ!! ↓ お掃除前 ↓ お掃除後
【週末お掃除】git に溜まった不要な操作履歴やデータを削除してストレージ容量を節約しようと試行錯誤した記録 これなに 昨今 AI によって git 管理の在り方自体見直され始めていますが、まだまだみなさん使っているであろう git について。 .git には過去の履歴や不要データがたくさん蓄積していき、気付けばかなり肥大化していてストレージを圧迫していることがあります。 自分のマシンは以下のような感じでかなり肥大化していてしんどいのでお掃除しようと思ったので、その作業記録を個人のメモとしてまとめました。 個人用のメモなのでいろいろ不足していたりすることもありますが、必要な人は参考にしていただけたらと思います。 対象読者 .git によってストレージが食われすぎていてしんどい自分のようなストレージにお金かけられない人たち 実行環境 macOS 27 Golden Gate 27.0.1 ⚠️ 注意 - 後述の手順を実行すると、完全に「元に戻せない(Undoできない)」状態になります。 - 本来は間違えてブランチを消しても reflog から復元できますが、後述するコマンド群を実行した後はリポジトリが極限まで軽量化される代わりに、過去のミスを救済するデータもすべて消去されます。大容量ファイルの削除作業の「最後の仕上げ」としてのみ実行してください。 - 作業を実行する前に、現在のリポジトリのバックアップをとるなどして安全策をとるようにしてください。 先にオチだけ 結論だけ先に書くと、最終的に新しいリポジトリ爆誕させてそこにファイルだけ引っ越しました🫠(なんだそりゃ) とはいうものの、実際何をやったのかをメモしましたので笑いながら読んでください。 初期調査 なんかよくわからないけど GitHub のリポジトリを入れているところがかなり肥大化している模様でした。 タイトルですでに git が問題だということはすでにわかってはいますが、そこに行き着くまでの調査記録を念のため書きます。 !image.png ディレクトリのサイズ確認 Applications, Developer などが肥大化するのはまあわかるのですが、 Documents がかなり食っていたのは気になったので調べてみると、 GitHub のディレクトリがかなり食っていることが判明。 !image.png du コマンドで調べてみたところこんな感じでした。 % du -sh ~/GitHub/* 1.4G /Users/su3-hokkaido/GitHub/su3-app01 20M /Users/su3-hokkaido/GitHub/su3-app02 5.1G /Users/su3-hokkaido/GitHub/su3-app03 1.4G /Users/su3-hokkaido/GitHub/su3-app04 4.0K /Users/su3-hokkaido/GitHub/su3-app05 4.0K /Users/su3-hokkaido/GitHub/su3-app06 4.0K /Users/su3-hokkaido/GitHub/su3-app07 1.1G /Users/su3-hokkaido/GitHub/su3-app08 4.0K /Users/su3-hokkaido/GitHub/su3-app09 4.0K /Users/su3-hokkaido/GitHub/su3-app10 7.1G /Users/su3-hokkaido/GitHub/su3-app11 207M /Users/su3-hokkaido/GitHub/su3-app12 % ここからさらに細かい原因を調べるために、特に肥大化が激しい app11 のディレクトリを調べてみることに。 .git 肥大化が判明 さらに分解して調べてみると一つ一つは全然重くなさそう。 % du -sh ~/GitHub/su3-app11/* 1.2M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge01 804M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge02 4.0K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge03 10M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge04 76K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge05 276K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge06 3.4M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge07 28K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge08 18M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge09 148K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge10 216K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge11 60M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge12 12K /Users/su3-hokkaido/GitHub/su3-app11/hogehoge13 1.1M /Users/su3-hokkaido/GitHub/su3-app11/hogehoge14 % もしかして?と思って .git を調べたらビンゴ。6GBも食っていました。 % du -sh .git/objects 6.3G .git/objects % お掃除作業手順(基本編) ここからは一番大きく、かつ別に履歴は必要としていないリポジトリ(約7GB食っていたリポジトリ)に対して作業していこうと思います。 セットアップ git-filter-repo をインストールします。(インストール手順はこちら: github.com/newren/git-filter-repo/blob/main/INSTA… ) 自分は homebrew 経由でインストールしました。 % brew install git-filter-repo ==> Auto-updating Homebrew... Adjust how often this is run with `$HOMEBREW_AUTO_UPDATE_SECS` or disable with `$HOMEBREW_NO_AUTO_UPDATE=1`. Hide these hints with `$HOMEBREW_NO_ENV_HINTS=1` (see `man brew`). ==> Downloading https://ghcr.io/v2/homebrew/core/portable-ruby/blobs/sha256:43ff285a82f48c84a06d0676a3800815a19d290d32e44182ddd7a6e95478e18e ######################################################################### 100.0% ==> Pouring portable-ruby-4.0.9.arm64_big_sur.bottle.tar.gz ==> Downloading bottle manifests ✔︎ Bottle Manifest git-filter-repo (2.47.0) Downloaded 7.3KB/ 7.3KB ==> Fetching downloads for: git-filter-repo ✔︎ Bottle git-filter-repo (2.47.0) Downloaded 101.9KB/101.9KB ==> Pouring git-filter-repo--2.47.0.all.bottle.1.tar.gz 🍺 /opt/homebrew/Cellar/git-filter-repo/2.47.0: 9 files, 361.5KB % 分析レポート作成 対象リポジトリのディレクトリで git filter-repo --analyze を実行して、お掃除前にどんなものが入っているか確認します。 実際の実行ログは以下のような形です。 % git filter-repo --analyze Processed 4009 blob sizes Processed 382 commits Writing reports to .git/filter-repo/analysis...done. % .git/filter-repo/analysis 配下に生成された各ファイルの詳細は省略しますが、ざっとのイメージは以下の通り。 詳しくは大元のリポジトリ git-filter-repo を読んだ方が確実かと思います。 - path-all-sizes.txt : 全部のファイル - path-deleted-sizes.txt : 過去に削除されたファイル - directories-all-sizes.txt : 全部のディレクトリ - directories-deleted-sizes.txt : 過去に削除されたディレクトリ 不要オブジェクトを掃除する 実行コマンドは以下の2つ。 1: 過去の操作履歴(リブログ)を今すぐ期限切れにする git reflog expire --expire=now --all - Gitは、コミットの切り替え(checkout)や打ち消し(reset)、ブランチの削除など、あなたがリポジトリ内で行った「すべての操作履歴」を裏で記録しています。これを reflog と呼びます。 - --expire=now: 本来なら一定期間(デフォルトで30〜90日間)保管される操作履歴を、「今すぐすべて期限切れ」にします。 - --all: すべてのブランチや参照の操作履歴を対象にします。 2: 不要になったデータ実体を完全に削除し、残ったデータを強力に圧縮する git gc --prune=now --aggressive - gc(ガベージコレクション)とは Git における「ゴミ集め・掃除」コマンドです。どのコミットやブランチ、操作履歴からもリンクされなくなった孤立したデータを検知して削除します。 - --prune=now: 通常、直近2週間に作られたゴミデータは誤操作防止のために残されますが、それを無視して「今あるゴミを今すぐすべて完全削除」します。 - --aggressive: 通常の最適化よりも、より時間をかけて強力にファイルを圧縮します。共通するコードやファイル差分を綺麗にまとめて、 .git フォルダの容量を限界まで削ります。 実際の実行ログは以下の通り。 時間の計測はしていないのですが、たぶん実行時間で言うと60分くらいかかったかも。 実行結果 % git reflog expire --expire=now --all % git gc --prune=now --aggressive Enumerating objects: 5420, done. Counting objects: 100% (5420/5420), done. Delta compression using up to 10 threads Compressing objects: 100% (5273/5273), done. Writing objects: 100% (5420/5420), done. Total 5420 (delta 2924), reused 1282 (delta 0), pack-reused 0 (from 0) % お掃除作業手順(発展編) 実行したけれども、100MB だけしか減っていなかった。。。 % du -sh .git/objects 6.2G .git/objects % 追加調査 git fsck --unreachable --dangling を実行して本当に孤立したゴミ(Dangling object)があるかを改めて確認したところ以下のようになった。 .DS_Store の話しか出てこなくて、消せそうなものが全く無し。 # コマンド: git fsck --unreachable --dangling % git fsck --unreachable --dangling error: refs/.DS_Store: badRefName: invalid refname format error: refs/.DS_Store: badRefContent: Checking ref database: 100% (1/1), done. Checking object directories: 100% (256/256), done. Checking objects: 100% (5420/5420), done. Verifying commits in commit graph: 100% (382/382), done. % そこでさらに容量を食っている上位10ファイル(コミット)を特定してみます。するといろいろ素材で使っていた動画や画像のコミットが!(これだ!) # コマンド: git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')" % git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.pack | sort -k 3 -n | tail -10 | awk '{print $1}')" b3d8ee51c7b74ae photos/canon/canon_eos200/IMG_5239.JPG b41cfacbd8297d5 photos/canon/canon_eos200/MVI_5303.MP4 e875256d0382801 photos/canon/canon_eos200/IMG_5322.JPG e704d9a01e0a9e2 photos/canon/canon_eos200/IMG_5323.JPG 320f19f210690ae photos/canon/canon_eos200/IMG_5324.JPG 1bb6700c6973c3a photos/canon/canon_eos200/IMG_5325.JPG d94566c2ad22e3f photos/canon/canon_eos200/IMG_5326.JPG fc3ab40e7884c53 photos/canon/canon_eos200/IMG_5327.JPG 38e68342661e8ce photos/canon/canon_eos200/MVI_5443.MP4 3ba184a31a09506 photos/canon/canon_eos200/MVI_5526.MP4 % そんなわけで画像、動画を削除しにいく --path で指定したフォルダを、歴史上から完全に除外( --invert-paths )してすべてのコミット履歴を自動で書き換えます。 冒頭でインストールした filter-repo を使って削除しに行きます。 git filter-repo --path photos/canon/canon_eos200/ --invert-paths 実行したけど破壊的な上書きだからフレッシュクローンしろと怒られました。 % git filter-repo --path photos/canon/canon_eos200/ --invert-paths Aborting: Refusing to destructively overwrite repo history since this does not look like a fresh clone. (expected at most one entry in the reflog for HEAD) Please operate on a fresh clone instead. If you want to proceed anyway, use --force. % しかし、ローカルでもう必要なものはバックアップとっているので、気にせず --force で強制実行。 % git filter-repo --path photos/canon/canon_eos200/ --invert-paths --force NOTICE: Removing 'origin' remote; see 'Why is my origin removed?' in the manual if you want to push back there. (was git@github:su3-hokkaido/su3-app11.git) Parsed 191 commits New history written in 0.21 seconds; now repacking/cleaning... Repacking your repo and cleaning out old unneeded objects HEAD is now at abc1234 hogehoge Enumerating objects: 4728, done. Counting objects: 100% (4728/4728), done. Delta compression using up to 10 threads Compressing objects: 100% (1885/1885), done. Writing objects: 100% (4728/4728), done. Total 4728 (delta 2707), reused 4722 (delta 2705), pack-reused 0 (from 0) Completely finished after 7.05 seconds. % これだけでもう 4GB くらい消えました。 # Before % du -sh .git/objects 6.3G .git/objects % # After % du -sh .git/objects 2.4G .git/objects % 最後にもう一度基本編で紹介したコマンドを実行して孤立データを削除しに行きます。 % git reflog expire --expire=now --all % git gc --prune=now --aggressive Enumerating objects: 4728, done. Counting objects: 100% (4728/4728), done. Delta compression using up to 10 threads Compressing objects: 100% (4590/4590), done. Writing objects: 100% (4728/4728), done. Total 4728 (delta 2700), reused 2008 (delta 0), pack-reused 0 (from 0) % が、あまり変わらず。 % du -sh .git/objects 2.4G .git/objects % git に再度 push しようにもデータ量が多すぎて、サーバーが耐えきれずに途中で 500 エラーになってしまいました。 % git remote add origin git@github.com:su3-hokkaido/su3-app11.git # filter-repo は origin を削除するので、リモートを再登録する必要あり % git push -f origin main Enumerating objects: 4728, done. Counting objects: 100% (4728/4728), done. Delta compression using up to 10 threads Compressing objects: 100% (1890/1890), done. error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500 send-pack: unexpected disconnect while reading sideband packet Writing objects: 100% (4728/4728), 2.43 GiB | 21.06 MiB/s, done. Total 4728 (delta 2700), reused 4728 (delta 2700), pack-reused 0 (from 0) fatal: the remote end hung up unexpectedly Everything up-to-date % ここから過去のコミットごとに push し直していくという作業をすれば解決する目処はついたものの、そんな気力はなく諦めました。 (スクリプト作って頑張ってもいいけど、労力的に無駄になりそう) 最終奥義 要件的には過去の reflog は全く必要なかったので「もういいや!」となって新しいリポジトリを爆誕させて必要なファイルだけそちらにお引越し。 最初からこっちでよかったね!ハハハハ!! ↓ お掃除前 !image.png ↓ お掃除後 !image.png
You Deleted the Leaked Key. Git Kept a Copy You deleted the leaked key. Git kept a copy. 6 commands find it. 1 erases it. Deleting a secret in a new commit doesn't remove it from Git. The old commits, the reflog and even a dropped stash still hold it. Every run is real, in a throwaway container against my own demo repo. The AWS key shown is fake (made up, never valid). Rule number one: rotate the key first. Rewriting history doesn't touch clones and forks that already have it. 1. git log -S: pickaxe search, every commit that added or removed a string. git-scm.com 2. git show: the commit that "deleted" the key still prints it in its diff. git-scm.com 3. git blame: the commit, author and time behind each line. git-scm.com 4. git reflog: a reset --hard commit is still in the reflog, and still readable. git-scm.com 5. git fsck --lost-found: a dropped stash becomes a dangling commit you can still read. git-scm.com 6. TruffleHog: scans every commit in the history for secrets. trufflesecurity/trufflehog 7. git filter-repo: the fix. Rotate the key, then rewrite every commit (--replace-text). newren/git-filter-repo GitHub's guide: docs.github.com Where the key should live instead, secrets managers and other DevSecOps tools: stationx.net Free web-book, Become the Cyber Security Expert the AI Era Demands: app.stationx.net Music: "Liberation" by Kraftamt, CC BY 3.0, ccmixter.org #git #devsecops #secrets #appsec #cybersecurity
API key in Git history? Deleting .env won't fix it #Shorts You deleted the API key. Git didn't. Every commit is a snapshot of all your files. Delete .env in a new commit and the old commit still has it: git show HEAD~1:.env prints the key. The fix, in this order: 1. Rotate the key: revoke it and issue a new one. GitHub's docs and the git-filter-repo manual both say to do this first. 2. Rewrite history: git filter-repo --sensitive-data-removal --invert-paths --path .env, then git push --force --mirror origin. This can't reach other people's clones or forks, and one git pull && git push from an old clone brings the file back. That's why step 1 comes first. 3. Put .env in .gitignore before the first commit. .gitignore doesn't affect files Git already tracks. On GitHub, push protection for your account is on by default and stops pushes of secrets to public repositories, for the key formats secret scanning recognizes. Repo, hashes and terminal output in the video come from a real example repo (Git 2.56). The key is fake. Sources: - GitHub Docs, Removing sensitive data from a repository: docs.github.com - git-filter-repo manual: github.com/newren/git-filter-repo/blob/main/Docum… - GitHub Docs, About push protection: docs.github.com - GitHub Docs, Supported secret scanning patterns: docs.github.com - Pro Git, What is Git?: git-scm.com - gitignore: git-scm.com Ever committed a secret? Yes or no. #Git #Programming #Shorts
Repositório oficial: newren/git-filter-repo
You Committed .env to GitHub: What to Rotate (Supabase + Next.js) You pushed, then your stomach dropped: .env is in the commit. Maybe a bot already emailed you. Take a breath — this is recoverable, and panicking into random git commands usually makes it worse. The single most important thing to understand: deleting the file does not fix anything. Once a secret is in git history, assume it is public forever. The real fix is rotation — making the leaked value useless by replacing it upstream. Purging history is cleanup you do after rotating, not instead of it. Let's go value by value through a typical Supabase + Next.js .env, sort out what's actually a secret, and rotate only what matters. First: which of these is actually secret? A typical .env looks like this: NEXT_PUBLIC_SUPABASE_URL=https://abcdxyz.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi... # role: anon SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi... # role: service_role SUPABASE_JWT_SECRET=super-secret-signing-key DATABASE_URL=postgresql://postgres:PASSWORD@db.abcdxyz.supabase.co:5432/postgres STRIPE_SECRET_KEY=sk_live_... RESEND_API_KEY=re_... Here's the part that saves you hours of unnecessary panic. Safe by design — do NOT need rotating: - NEXT_PUBLIC_SUPABASE_URL — your project URL is public. It's in every network request the browser makes. - NEXT_PUBLIC_SUPABASE_ANON_KEY — the anon (publishable) key is public by design. It ships in your client bundle on purpose. Any NEXT_PUBLIC_ variable is inlined into the browser JavaScript at build time, so it was never a secret to begin with. The anon key's role is anon. It has no special power on its own — it's just an identity meaning "an unauthenticated (or logged-in) user of this project." What protects your data when that key is used is Row Level Security (RLS). If your tables have RLS enabled with correct policies, a leaked anon key is a non-event. If they don't, that key was already exposed to every visitor of your site — the git commit changed nothing. Actually secret — rotate every one of these: - SUPABASE_SERVICE_ROLE_KEY - SUPABASE_JWT_SECRET - DATABASE_URL (the Postgres password inside it) - Every third-party key: Stripe, Resend, OpenAI, etc. The service_role key is the scary one. Its role is service_role, and it bypasses RLS entirely — full read, write, and delete on every table, plus Storage. It's a server-only secret. It must never appear in a browser, a public repo, or anything with a NEXT_PUBLIC_ prefix. If it leaked, treat it as a full database breach until rotated. How to tell the two keys apart Both are JWTs, so they look identical at a glance. Decode the middle segment (base64URL) and read the role claim: # paste your key in place of PASTE_KEY echo 'PASTE_KEY' | cut -d. -f2 | base64 -d 2>/dev/null | python -m json.tool You'll see either "role": "anon" (safe, public) or "role": "service_role" (secret, rotate now). Newer Supabase projects use clearer prefixes instead of JWTs: sb_publishable_... (public) vs sb_secret_... (secret). The prefix tells you everything — no decoding needed. Rotating each secret The Supabase keys In the dashboard, go to Project Settings → API Keys. How rotation works depends on which key system your project uses: - New keys (sb_publishable_ / sb_secret_): these are independent of each other and of the JWT secret. Revoke the leaked sb_secret_ key and create a new one; the publishable key is unaffected because it wasn't a secret. - Legacy JWT keys: both the anon and service_role keys are JWTs signed by your project's JWT secret. Rolling that secret regenerates both keys at once, because both are signatures over it. Either way, after rotating the secret: 1. Copy the new service_role / sb_secret_ key into your server environment (Vercel project env vars, .env.local, etc.). 2. If you rolled a legacy JWT secret, also grab the regenerated anon key and redeploy so the fresh key ships in your bundle. One consequence to expect on the legacy path: rolling the JWT secret invalidates every JWT signed with the old one, which logs out all existing user sessions. That's the correct, safe outcome after a leak — do it anyway. Database password Project Settings → Database → Reset database password. This changes the password embedded in your DATABASE_URL / connection string. Update that string everywhere it lives (Vercel, CI secrets, local .env.local, migration tooling). Anything still using the old password starts failing — that's your checklist of places to fix. Third-party keys Each provider has its own flow, but the pattern is identical: generate a new key, deploy it, then revoke the old one. Don't skip the revoke — a new key does not disable the leaked one. - Stripe: Developers → API keys → roll the leaked secret key. - Resend / OpenAI / etc.: create new key, swap in env, delete old. Now purge git history Rotation makes the leaked values worthless. Purging history is about hygiene and not re-leaking on the next clone. Deleting the file in a new commit is not enough — the old commit still contains it. The clean tool is git filter-repo: pip install git-filter-repo # remove the file from all of history git filter-repo --path .env --invert-paths Or with BFG: bfg --delete-files .env git reflog expire --expire=now --all && git gc --prune=now --aggressive Then force-push the rewritten history: git push origin --force --all git push origin --force --tags Two honest caveats: force-pushing rewrites history for every collaborator (they'll need to re-clone or reset), and GitHub caches commits — forks and cached commit views can retain the blob even after your push. That's exactly why rotation is the real fix and history-purging is secondary. Then add .env to .gitignore so this can't recur: echo ".env" >> .gitignore echo ".env.local" >> .gitignore git rm --cached .env # stop tracking without deleting your local copy The part everyone skips: is your RLS actually protecting you? Here's the uncomfortable truth. If a leaked anon key genuinely exposes your data, the anon key wasn't the vulnerability — your RLS was. That key is public to every visitor regardless of any git leak. A few things worth checking on every table: - RLS is enabled on the table. Enabling it with no policies denies all access through the API — that's fail-safe, not broken. - A role needs both a matching policy and the underlying table GRANT. - A policy with no TO clause applies to every role, including anon. - USING (true) on a SELECT policy makes the table world-readable. It's only world-writable if a permissive INSERT/UPDATE/ALL policy also exists (INSERT policies gate rows with WITH CHECK, not USING). - For an anonymous request, auth.uid() is NULL — so USING (auth.uid() = user_id) correctly returns nothing to anon. Quick audit of what's exposed: -- tables with RLS OFF (guarded only by GRANTs, no policy gate) select relname from pg_class where relkind = 'r' and relnamespace = 'public'::regnamespace and not relrowsecurity; -- every policy, so you can eyeball no-TO and USING(true) select schemaname, tablename, policyname, roles, cmd, qual, with_check from pg_policies where schemaname = 'public' order by tablename; If reading those results makes you nervous, I put together a free, read-only repro and audit kit here: cekuu35/supabase-rls-leak-demo. It has a deliberately leaky schema plus copy-paste SQL that flags world-readable tables and no-TO policies, so you can see exactly what an anon key can reach in your own project. The 60-second recap 1. Don't panic, don't just delete the file. 2. Anon key + project URL + NEXT_PUBLIC_ vars: safe, no rotation needed. 3. service_role, JWT secret, DB password, third-party keys: rotate now, revoke the old ones. 4. Purge history with filter-repo/BFG and force-push — but only after rotating. 5. Audit your RLS, because that's what actually stands between the public anon key and your data. If you want to go deeper than the free audit — a full policy-by-policy checklist, the common false-positive traps, and fix templates — I keep a $29 RLS Audit Kit that packages it all up. Totally optional; the free demo repo above is enough to check and fix most projects on your own. Either way: rotate first, breathe second, and get RLS right so the next leak is a shrug instead of a scramble.
過去にコミットした.envはgitの履歴から読める:見つけ方と git-filter-repo での消し方(git 2.47で確認) 結論 - .gitignore に足して git rm --cached .env しても、1つ前のコミットからは git show HEAD~1:.env で中身が出ました(筆者の環境で確認)。 - 履歴に残っているかは git log --all --full-history -- '.env*' '**/.env*' などで探せます。消すときは git filter-repo --sensitive-data-removal --invert-paths --path .env を、新しく取ったクローンで実行します(GitHub公式の手順)。 - 履歴を書き換えても、漏れた鍵は無効になりません。最初にやるのは鍵の再発行(ローテーション)です(GitHub公式)。書き換え前のクローンには、消したはずの .env がそのまま残っていました(筆者の環境で確認)。 > この記事は「AIエージェントと秘密情報」の検証シリーズです。新しい検証は X(@supportapp3_lab)でもお知らせしています。 この記事は、AIエージェントと秘密情報の話の続きです。Codex CLIに.envを読ませない設定 では、.env の読み取りを deny しても git show HEAD:.env で中身が出ることを確かめました。deny は「いま作業ツリーにあるファイル」を守る設定で、git の中に残った同じ内容までは守りません。今回はその穴を、履歴の側から塞ぎます。 検証環境と範囲 - Debian 13 / git 2.47.3 / git-filter-repo 2.47.0(pip で一時的な venv に導入) - GitHub ではなく、ローカルの bare リポジトリを origin に見立てて確かめました。 - 置いた値はすべてダミーです(DUMMY_API_KEY=dummy_not_a_secret_123 など)。検証後に削除しました。 確かめていないこと - GitHub 上での挙動(キャッシュされた表示、PR 参照、フォーク)。この部分は GitHub 公式ドキュメントの記載だけを紹介します。 - macOS と Windows での挙動。 - Claude Code で Read(./.env) を deny したときに git show HEAD~1:.env が止まるか(後述)。 1. 「消したつもり」の状態を作る .env と config/.env.local をコミットしてから、よくある手順で「管理対象から外し」ました。 git add . && git commit -m "initial commit" # うっかり .env ごとコミット git rm --cached .env config/.env.local printf '.env\n.env.*\n' > .gitignore git add .gitignore && git commit -m "stop tracking .env" この状態で試した結果です。 $ git status --short --ignored !! .env !! config/ $ git show HEAD:.env fatal: path '.env' exists on disk, but not in 'HEAD' $ git show HEAD~1:.env DUMMY_API_KEY=dummy_not_a_secret_123 最新のコミットには .env がありません。でも1つ前のコミットを指定すると、中身がそのまま出ました。push 済みなら、リモートにも同じ履歴があります。「最新のコミットに無い」は「安全」ではありません。 2. 自分のリポジトリを点検する3つのコマンド ① .env を追加・削除したコミットを探す $ git log --all --full-history --oneline -- '.env*' '**/.env*' 74d5f5a stop tracking .env 03f2157 initial commit --all で全ブランチとタグ、--full-history で履歴の簡略化をせずに探します。何も出なければ、その名前のファイルはコミットされていません。 ② 履歴の中にある .env 系のファイルを一覧にする $ git rev-list --all --objects | grep -E '(^| |/)\.env' 17f709d7339578e5b4b54e9d4153693ddf609837 .env 7de58cddc49b61f742d30ac9d03fb9fb8e9cff12 config/.env.local 左の値はファイルの中身(blob)のハッシュです。あとで「本当に消えたか」を確かめるときに使います。 ③ 値そのものを、全コミットから探す $ git grep -n dummy_ $(git rev-list --all) 03f2157484012a8b12d24cd07642ed29f810b0c7:.env:1:DUMMY_API_KEY=dummy_not_a_secret_123 03f2157484012a8b12d24cd07642ed29f810b0c7:config/.env.local:1:DUMMY_DB_PASS=dummy_local_456 ファイル名を変えてコミットしてしまった場合も、これなら見つかります。本物の鍵を探すときは、鍵の値そのものをコマンドに打たないでください。シェルの履歴に残ります。API_KEY= のような変数名で探すのがおすすめです(筆者の提案)。 おまけ:stash にも残る git stash -u は ignored のファイルを含みません。筆者の環境では、変更が .env だけだったので stash 自体が作られませんでした。一方、git stash --all は ignored のファイルも含めるので、.env が stash に入りました。 $ git stash -u No local changes to save $ git stash --all Saved working directory and index state WIP on main: 74d5f5a stop tracking .env $ git show 'stash@{0}^3:.env' DUMMY_API_KEY=dummy_changed_789 stash はローカルにしか残らず、普通の push では送られません。ただ、AIエージェントが git show で読める場所が1つ増えます。 3. 見つかったら:やる順番 ① 先に鍵を再発行する GitHub の公式ドキュメントは、秘密情報なら最初に revoke や rotate をするよう書いています。再発行すれば古い鍵はもう使えないので、それで十分なら、履歴の書き換えまではしなくてよい場合もあるとも書かれています。 履歴を書き換えても、それまでに誰かが取得した鍵は使えるままです。順番を逆にしないでください。 ② 新しく取ったクローンで git-filter-repo を実行する --sensitive-data-removal は git-filter-repo 2.47 以上で使えます(GitHub公式)。 最初に、作業していたリポジトリでそのまま実行してみました。すると、こう言われて止まりました。 $ git filter-repo --sensitive-data-removal --invert-paths --path .env --path config/.env.local Aborting: Refusing to destructively overwrite repo history since this does not look like a fresh clone. (expected at most one entry in the reflog for HEAD) Note: when cloning local repositories, you need to pass --no-local to git clone to avoid this issue. Please operate on a fresh clone instead. If you want to proceed anyway, use --force. GitHub の手順も、まずクローンしてからその中で実行する流れです。新しくクローンして実行し直しました(ローカルのリポジトリからクローンしたので --no-local を付けています)。 $ git clone -q --no-local origin.git fresh $ cd fresh $ git filter-repo --sensitive-data-removal --invert-paths --path .env --path config/.env.local NOTICE: Fetching all refs from origin to make sure we rewrite all history that may reference the sensitive data, via git fetch -q --prune --update-head-ok --refmap "" origin +refs/*:refs/* Parsed 2 commitsHEAD is now at db361b5 stop tracking .env New history written in 0.03 seconds; now repacking/cleaning... You rewrote 2 (of 2) commits. NOTE: First Changed Commit(s) is/are: 03f2157484012a8b12d24cd07642ed29f810b0c7 NOTE: LFS object orphaning not checked (LFS not in use) (以下、次の手順の案内が続く) - --path には、ファイル名だけではなくリポジトリ内のパスを書きます。移動や改名をしたことがあるなら、前のパスも --path で足してください(GitHub公式)。 - First Changed Commit(s) の値は、GitHub Support に連絡するときに使います(GitHub公式)。 ③ 消えたか確かめる $ git log --all --full-history --oneline -- '.env*' '**/.env*' $ git grep -n dummy_ $(git rev-list --all) $ git cat-file -p 17f709d7339578e5b4b54e9d4153693ddf609837 fatal: Not a valid object name 17f709d7339578e5b4b54e9d4153693ddf609837 $ git log --oneline db361b5 stop tracking .env 9278ca3 initial commit 探すコマンドは2つとも何も出さなくなり、2章②で控えた blob のハッシュも見つからなくなりました。コミットのハッシュは全部変わっています。 --sensitive-data-removal を付けた今回は、origin の設定が残りました(git remote -v で確認)。別の新しいクローンで、このオプションを付けずに実行したときは origin の設定が消えました。次の push の前に git remote -v を見ておくと安全です(筆者の環境で確認)。 ④ force push する $ git push --force --mirror origin To /path/to/origin.git + 74d5f5a...db361b5 main -> main (forced update) (パスは書き換えています) GitHub では、refs/pull/ で始まる参照は読み取り専用なので、push が失敗します(GitHub公式)。ブランチ保護で force push を禁止しているなら、一時的に外す必要があります。 4. 消しても残る場所 ① 書き換え前のクローン(実機で確認) 書き換え前に作っておいた別のクローンでは、旧 blob も旧コミットもそのまま読めました。 $ git cat-file -p 17f709d7339578e5b4b54e9d4153693ddf609837 DUMMY_API_KEY=dummy_not_a_secret_123 $ git show HEAD~1:.env DUMMY_API_KEY=dummy_not_a_secret_123 GitHub 公式によると、古いクローンを持つ人が書き換え後に git pull して git push すると、秘密情報がまた戻ってきます。古いクローンは捨ててクローンし直してもらうのが確実です。 ② リモート側にも、しばらく残る(実機で確認) force push した直後の bare リポジトリでは、旧 blob がまだ残っていました。git gc --prune=now を実行して、やっと消えました。 $ git --git-dir=origin.git cat-file -t 17f709d7339578e5b4b54e9d4153693ddf609837 blob $ git --git-dir=origin.git gc -q --prune=now $ git --git-dir=origin.git cat-file -t 17f709d7339578e5b4b54e9d4153693ddf609837 fatal: git cat-file: could not get object info これは自分で管理する bare リポジトリでの結果です。GitHub ではサーバー側の gc を自分では実行できません。 ③ GitHub 上(公式の記載のみ。実機では未確認) GitHub 公式によると、書き換えて force push したあとも、次の場所からは読めます。 - クローンとフォーク - SHA を直接指定した、GitHub 上のキャッシュされた表示 - そのコミットを参照している PR キャッシュされた表示と PR の参照は、GitHub Support に頼めば消してもらえます。ただし、鍵のローテーションでリスクを抑えられる場合は対応しないと明記されています。フォークに残ったものは、フォークの持ち主に頼むしかありません。 5. AIエージェントの deny では止まらない理由 Claude Code の公式ドキュメントでは、Read の deny が効く範囲を次のように説明しています。 - 組み込みのファイルツールと、Bash で認識できるファイルコマンド(cat、head、tail、sed、tee)、リダイレクトの対象には効く。 - ファイル名を指定せずに読むコマンドや、スクリプトのように間接的にファイルを読むサブプロセスには効かない。OS レベルで止めたいなら sandbox を使う。 - git の読み取り系のコマンドは、確認なしで実行される組み込みの読み取り専用コマンドに含まれる。 git show HEAD~1:.env は、作業ツリーの .env ではなく .git の中のオブジェクトを読みます。なので Claude Code で Read(./.env) を deny していても、止まらない可能性があります。ただし、これは公式の記載から考えた推測で、実機では確かめていません。 Codex CLI では、deny を設定しても git show HEAD:.env で読めたことを 前回の記事 で確認しています。 AI に読ませたくない鍵が履歴に一度でも入ったなら、deny の設定より先に、鍵の再発行が必要です。 6. 再発防止 GitHub 公式が挙げている方法です。 - .gitignore に .env を書き、そのファイル自体もコミットして共有する - git add . や git commit -a を避け、ファイルを1つずつ追加する。git diff --cached で中身を見てからコミットする - pre-commit フックで秘密情報を検出する(git-secrets や gitleaks など) - GitHub の push protection を有効にする AIエージェントの側では、次の設定も合わせて行ってください。 - Claude Code: settings.json の permissions.deny - Codex CLI: permission profile の deny - どのエージェントが .env を読めそうかの点検: ファイル名だけで点検する日本語CLI まとめ - git rm --cached と .gitignore だけでは、過去のコミットに .env が残る(筆者の環境で確認)。 - git log --all --full-history、git rev-list --all --objects、全コミットへの git grep で探せる。 - 見つかったら、まず鍵を再発行する。そのうえで必要なら、新しく取ったクローンで git filter-repo --sensitive-data-removal を実行し、force push する。古いクローン、フォーク、GitHub 上のキャッシュには残る(クローンは実機、それ以外は公式)。 関連記事: - Codex CLIに.envを読ませない設定:公式例の "**/*.env" だけでは .env.local と深い階層が読めた - Claude Codeに.envを読ませない設定は settings.json の permissions.deny に書く - Claude Codeの会話記録は ~/.claude/projects/ に平文で30日残る - AIエージェントに.envを読まれていないか、ファイル名だけで点検する日本語CLIを作った 参考(公式): - Removing sensitive data from a repository – GitHub Docs - git-filter-repo(newren/git-filter-repo) - git-stash ドキュメント - Configure permissions – Claude Code Docs ※ 仕様は 2026-10-19 時点の公式ドキュメントに基づきます。検証は git 2.47.3 / git-filter-repo 2.47.0 で、Linux の使い捨て環境でのみ行いました。 試してみる この記事の①の点検をまとめて行うCLIを公開しました(MIT License): supportapp3-lab/ai-secret-check npx github:supportapp3-lab/ai-secret-check scan-history ./調べたいリポジトリ 全ブランチ・stash を含む履歴から、どの階層の .env / .env.* が追加されたかを、コミットの短いハッシュと件数だけ表示します。ファイルの中身は取り出しません。見つかったら、まず鍵の再発行から始めてください。 更新情報やちょっとしたTipsは X(@supportapp3_lab)で発信しています。
An AI Coding Agent Uploaded Whole Git Histories: How to Lock Down Your Homelab On 18 September 2026 a developer called ferstar published a teardown of ZCode, the desktop coding app from Z.ai (Zhipu AI) built on the GLM models, and the Hacker News thread that followed put it in front of everyone. While clearing disk space, ferstar noticed ~/.zcode holding more than 700 MB, unpacked the app, and found that a logged in client was packaging workspace snapshots and uploading them to Alibaba Cloud object storage. The snapshots were not just the source tree: in one 313 MB example covering 42,411 files, 86.6 percent of the payload was the .git directory, LFS objects and reflogs included. Each snapshot was encrypted with a key wrapped to a server held certificate, so the user could not decrypt their own upload. AI coding tools that run locally or integrate into your editor have deep filesystem access by design, and that access is increasingly being used in ways that are not obvious from the UI. If you run a homelab and use any AI agent that touches your code, this is your wake-up call. The resolution first, because it changes how you should read the rest. Z.ai responded the same day, initially attributing the uploads to a wiki generation feature. It deleted the Alibaba Cloud bucket on 20 September and on 21 September published ZCode's source under Apache 2.0 at zai-org/ZCode, citing third party audits. ferstar's follow-up review of the published code found the upload endpoint, the object storage path and the key delivery code all absent from version 3.14.0 onward, with indexing running on locally bundled tools. So the specific behavior is gone. What nobody can audit is what happened to whatever was uploaded before 18 September, which is exactly why the cleanup below is still worth doing. Now: why homelabbers are a soft target, and what to do about it. 💡 Why This Matters for Homelabbers Your homelab repos are not just toy projects. They often contain .env files, hardcoded API keys, internal IP ranges, VPN configs, Ansible inventories, and Docker Compose files with credentials baked in. Git history makes this worse because a secret you deleted three commits ago is still fully recoverable from the log. AI coding agents are designed to read your project context, and that context is your entire repo tree. When an agent "understands your codebase," it is reading files and history. The question is whether it keeps that data local or phones home with it. 🧰 Prerequisites - A Linux or macOS machine running AI coding tools (ZCode, Cursor, the GitHub Copilot coding agent, Cline, Continue, etc.). If you see GitHub Copilot Workspace referenced in older guides, that preview was sunset on 30 May 2025 and its ideas moved into the Copilot coding agent. - Basic comfort with the terminal and Git - Optional: a network monitoring tool like mitmproxy, Wireshark, or a Pi-hole with logging enabled 🔍 Step 1: Audit What Your AI Tools Are Actually Sending Before you lock anything down, you need to know what is leaving your machine. The bluntest tool here is watching outbound connections while your AI agent is active. On Linux, use ss or nethogs to watch live traffic per process: # Watch outbound connections from a specific process name ss -tp | grep zcode # Or monitor per-process bandwidth in real time sudo nethogs If you want to inspect the actual payload, route the tool's traffic through mitmproxy. Install it, set your HTTP_PROXY and HTTPS_PROXY environment variables to point at it, and trust its certificate. Then launch the AI agent from the same shell session and watch what it posts. Check the official docs for your specific tool to understand whether it respects proxy settings. export HTTP_PROXY=http://127.0.0.1:8080 export HTTPS_PROXY=http://127.0.0.1:8080 # Then launch your agent in the same terminal 🔧 Step 2: Scrub Secrets from Git History Before They Can Be Harvested If you have ever committed a secret to a repo that an AI agent can read, treat that secret as compromised. Rotate it first, then clean the history. The two most practical tools for this are git filter-repo and BFG Repo Cleaner. Check each project's documentation for the exact flags, since the syntax differs and getting it wrong can corrupt history. A basic git filter-repo invocation to strip a file from all history looks like this: # Install git-filter-repo first (see https://github.com/newren/git-filter-repo) git filter-repo --path secrets.env --invert-paths After rewriting history, force-push to all remotes and immediately rotate any exposed credentials. Do not skip the rotation step. Cleaning history only helps for future reads. 🛡️ Step 3: Use a .gitignore (and a Global One) The fastest mitigation is making sure sensitive files never enter Git in the first place. Add a thorough .gitignore to every project and also configure a global gitignore that covers your whole machine. # Set a global gitignore git config --global core.excludesFile ~/.gitignore_global # ~/.gitignore_global: common patterns to exclude .env .env.* *.pem *.key id_rsa id_ed25519 secrets/ vault_pass.txt ansible/group_vars/all/vault.yml This does not protect history that already exists, but it prevents new secrets from being committed accidentally, which is the most common source of leaks. 🛡️ Step 4: Sandbox AI Agents with Network Policies If you want a harder boundary, run AI coding tools inside a sandboxed environment that limits outbound network access. There are a few practical ways to do this in a homelab. - Docker with network restrictions: Run the agent in a container with --network none or a custom bridge that only allows connections to your LAN and a specific AI API endpoint. - Firewall rules per user: On Linux, you can use iptables or nftables to restrict outbound traffic from a specific Unix user that you create just for running the agent. - A dedicated VLAN: If you have managed switches and a router that supports VLANs (like pfSense or OPNsense), put your dev machine on a VLAN where you can log and filter all outbound connections at the firewall level. The VLAN approach is the most powerful because it works regardless of what the agent does inside your OS. You see everything at the network edge. 🛡️ Step 5: Prefer Local or Self-Hosted Models Where Possible The cleanest solution to data exfiltration is keeping data on your own hardware. Tools like Ollama let you run capable open-source models locally and expose them on a local API endpoint. Many AI coding frontends (Continue, for example) can be pointed at a local Ollama instance instead of a cloud API. # Example: point Continue or a compatible tool at a local Ollama endpoint # In the tool's config, set the API base URL to: http://localhost:11434 You give up some model capability compared to frontier cloud models, but for sensitive internal codebases the trade-off is often worth it. Check the documentation for whichever coding agent you use to confirm it supports custom API base URLs and what model format it expects. 🔧 Troubleshooting - mitmproxy shows no traffic from the agent: Some tools use certificate pinning or ignore system proxy settings. Try monitoring at the network level with Wireshark on your loopback or default interface instead. - git filter-repo says "not a git repository": Make sure you are running the command from inside the repo root, and that you have the tool installed correctly. It is not bundled with Git by default. - Ollama model responses are slow: This is normal on CPU-only hardware. Quantized models (Q4 or Q5) give you a better speed and quality balance on machines without a dedicated GPU. Check the Ollama model library for available quantization levels. ✅ Wrap Up The ZCode incident is a reminder that AI coding agents are powerful precisely because they have broad access to your project context, and that access can be abused, intentionally or accidentally. ZCode itself was patched and opened up within days, so treat this as a warning about the category rather than about one product, and note what it took to find out: a paying user had to measure a directory, unpack an Electron bundle and watch the traffic. Your homelab repos deserve the same scrutiny you would give a production environment. Start by auditing what your current tools are sending, scrub any secrets from Git history, and put network-level controls in place so you can see and limit outbound connections. If you are dealing with genuinely sensitive internal code, a local model via Ollama is worth the setup time. These are not exotic measures. They are just good hygiene for an era where your code editor is also a networked agent. --- Related from the lab - Better Homelab Secret Management with Infisical - An AI Agent Ran Up a $6,531 AWS Bill Scanning DN42. Here's How to Cap Yours - CrowdSec: Community-Powered Security --- Originally published at codewithromi.com.
The First Cut: How Module Boundaries Get Drawn and Why They Drift Part 2 of the Production Codebase series. New here? Start with Part 1. Software Architecture from First Principles — Part 2: Monolithic Entanglement, Coupling Mathematics, and Language-Enforced Boundaries --- The Thirty-Second Test Clone a healthy production repository, open the root directory in your editor, and inspect the file tree. Within thirty seconds, without inspecting a single function or reading a single implementation file, the operational reality of the system reveals itself. You know where domain business logic lives, where tests are segregated, which team owns the payment processor, how to spin up dependencies locally, which environment variables are mandatory, and how database migrations are sequenced. You have not sent a single message on Slack. You have not interrupted a tech lead. The physical structure of the repository answered your questions before you had to ask them. In the first article, we established that folder structure is a system design decision — shaped by Conway's Law, forcing functions, and team communication patterns. We introduced the fintech engine and promised to open it. This is that article. Now open the other kind of repository. Forty-three files sit strewn across the top-level directory. You see FEATURE_240_DESCRIPTION.md, analytics.ts, fin.java, and IMPLEMENTATION_COMPLETE_SUMMARY.md mixed directly alongside package.json and tsconfig.json. Three separate docker-compose files exist with names like docker-compose.local.yml, docker-compose.dev.backup.yml, and docker-compose.final.yml, none of which document which one actually works. The .env.example file contains stale database credentials from four years ago that reference a Postgres instance that was migrated to Amazon Aurora during the pandemic. You do not begin coding. You close your terminal, open Slack, and ask who knows how to run this project. The difference between those two repositories has nothing to do with the algorithmic brilliance of the code inside them. It has everything to do with whether the engineers who built the system treated the root directory as an intentional architectural contract or as an unmonitored staging ground for sprint artifacts. fintech-engine/ ├── .github/ # CI workflows and templates │ ├── workflows/ # Validation and deployment pipelines │ └── pull_request_template.md ├── .gitignore # Source control boundary ├── .dockerignore # Build context filter ├── .env.example # Local execution contract ├── CODEOWNERS # Ownership and compliance control ├── LICENSE ├── README.md # Entry point and setup guide ├── CHANGELOG.md # Human-readable change history ├── justfile # Task runner interface ├── docker-compose.yml # Local dependency topology ├── docs/ │ ├── adr/ # Immutable decision records │ ├── architecture/ # System maps and diagrams │ ├── onboarding/ # New engineer ramp-up guides │ └── runbooks/ # Incident playbooks ├── infra/ │ ├── terraform/ # Cloud resource provisioning │ └── k8s/ # Container scheduling manifests ├── migrations/ # Immutable schema scripts ├── scripts/ │ ├── local/ # Workstation scripts │ └── ci/ # Pipeline scripts ├── src/ # Application source code └── tests/ ├── unit/ # Fast in-memory tests ├── integration/ # Database and broker tests └── e2e/ # End-to-end journey tests --- The Locational Predictability Imperative There is an unspoken assumption among junior developers that good engineers read every line of code in the systems they maintain. In a real production system containing one hundred thousand or five hundred thousand lines of code, nobody reads every line of code. Tech leads, staff architects, and on-call responders survive by reading almost none of it. They navigate systems by forming coarse mental maps of ownership, boundaries, and lifecycles, diving into precise implementation lines only when an incident strikes or a specific interface requires extension. Empirical software engineering research confirms this reality. In their landmark study on developer activity, I Know What You Did Last Summer: An Investigation on How Developers Spend Their Time, researchers Roberto Minelli, Andrea Mocci, and Michele Lanza tracked developers across professional tasks and discovered that programmers spend 58 to 70 percent of their working time comprehending, navigating, and inspecting codebases, while spending only 5 percent of their time actively editing or typing code. Decades earlier, Robert C. Martin observed in Clean Code that the ratio of time spent reading code versus writing code exceeds ten to one. When a junior engineer submits a pull request introducing a new merchant discount rule, a senior reviewer should not have to execute a full-text search across fifty directories to determine where the logic was placed. A well-designed codebase exhibits locational predictability: given a business domain concept or a bug description, any experienced engineer should be able to deduce the exact directory, file, and interface where the modification belongs before opening the file tree. When a codebase lacks locational predictability, every code review becomes an archaeological expedition, and the seventy-percent navigation tax compounds into organizational paralysis. --- The Physical Geography of Repository Contracts The root of a production repository houses the physical contracts governing how code is built, tested, and audited, beginning with src/ to isolate application domain logic from development scaffolding. Yet treating src/ as a universal convention overlooks other language ecosystems. In Go repositories, binaries live in cmd/ while internal libraries reside in internal/. Introduced by Russ Cox in Go 1.4, any package inside an internal/ directory is mechanically restricted by the compiler: packages outside the immediate parent hierarchy cannot import it. While teams working in TypeScript or Python frequently rely on fragile lint rules that erode under sprint panic, Go turns directory placement into a compiler-enforced boundary. Database transformations inside migrations/ operate under a single non-negotiable law: applied migrations are immutable. Once a schema migration script is merged to the main branch and executed against any shared environment, that file must never be modified. Migration frameworks like Flyway enforce this discipline by calculating cryptographic checksums of every script and verifying them against a history ledger table, aborting deployments if a single byte has changed. To manage zero-downtime schema evolution safely across rolling releases without table-locking outages, teams apply the expand-contract pattern formalized by Pramod Sadalage and Martin Fowler, decoupling schema expansions, dual-writing code deployments, data backfills, and contracting column removals into distinct, non-breaking phases. Repository governance centers around CODEOWNERS, which performs three simultaneous architectural jobs. First, it routes pull requests to designated domain experts based on path matching. Second, it acts as a regulatory compliance control for Segregation of Duties (SoD) under Section 404 of the Sarbanes-Oxley Act (SOX) and SOC 2 Type II Common Criteria CC6.1, mathematically preventing authors from self-approving changes to sensitive financial calculation or authorization paths. Third, it serves as an automated blast-radius audit: any directory lacking an explicit code owner represents an unmonitored path that can bypass mandatory security and architectural reviews. The defensive perimeter of the repository concludes with .env.example, .gitignore, and .dockerignore. The .env.example file is an executable security contract that defines mandatory environment keys, credential origins, and safe local defaults without exposing live secrets. Behind it, .gitignore protects against accidental secret leakage into source control—where committing a credential requires immediate rotation and history rewriting with git-filter-repo, since git packfiles retain deleted files indefinitely. Alongside it, .dockerignore prevents build daemons from leaking local environment files into distributable image layers, backed by pre-commit scanners like TruffleHog or Gitleaks to intercept credentials before they enter git history. --- The AI Code Generation Paradox This discipline matters more in 2026 than it ever has — because the next reader of your repository root is not always human. In 2026, the marginal cost of writing code has effectively dropped to zero. Any developer or autonomous agent can invoke a modern language model and generate five hundred lines of syntactically flawless implementation in seconds. Teams celebrate the illusion of velocity because tickets move across sprint boards faster than ever before. Yet software engineering has never been governed by the speed of syntax generation; it has always been governed by the economics of maintenance. A comprehensive empirical investigation, Coding on Copilot: 2023 Data Shows Downward Pressure on Code Quality by GitClear, analyzed 153 million lines of code written between 2020 and 2023. Their research revealed that code churn projected to double compared to pre-AI baselines. Simultaneously, the proportion of copy-pasted code rose dramatically, while deliberate refactoring, code movement, and deduplication plummeted. The mechanism driving this trend is structural. Large language models operate within finite context windows and optimize for local completion rather than global coherence. When an AI assistant or autonomous coding agent is asked to implement a feature in a sprawling repository with ambiguous directory boundaries, it suffers from context poisoning and prompt pollution: scanning the repository root ingests stale handoff documents and conflicting configuration artifacts into the agent's working memory, which the model treats as authoritative ground truth. Rather than constructing a unified domain abstraction, the agent takes the path of least resistance: it duplicates utility helpers, introduces direct cross-module couplings, or dumps ephemeral scripts into the root folder. Without rigid physical boundaries and explicit architectural constraints, AI coding assistants do not eliminate technical debt; they accelerate repository decay at ten times human speed. In the AI era, architecture is no longer merely a human convention. It is the structural fence that prevents automated agents from drowning a production system in unmaintainable sludge. --- The Morning After the Folder Reorganization Now assume your team executed this root discipline flawlessly. You cleaned up root sprawl, established immutable migrations, standardized workstation automation with just, decoupled deployment manifests per ArgoCD guidelines for a separate config repo, and created crisp domain directories inside src/: /auth/, /transactions/, /ledger/, and /webhooks/. The pull request merged, and the team celebrated. Then, three weeks later, a critical merchant integration deadline loomed at 4:30 PM on a Thursday. A senior engineer needed to verify whether a customer had an active KYC flag before allowing a webhook retry. The KYC verification lived in /auth/. The webhook retry logic lived in /webhooks/. Under deadline pressure, the engineer did not design an asynchronous domain event or an anti-corruption interface. They simply typed: import { verifyCustomerKYC } from '../auth/service'; The pull request passed code review because the reviewer was rushing to cut the sprint release. It passed continuous integration because the TypeScript compiler resolved the path without issue. With a single keystroke, the architectural boundary was punctured. Six months later, /auth/ imported /transactions/, /transactions/ imported /ledger/, /ledger/ imported /webhooks/, and /webhooks/ imported /auth/. On disk, the repository appeared neatly partitioned into four domain folders. In memory, the application had collapsed into a tangled, circular distributed hairball. !Diagram 10: Week 1 Intended Decoupling vs. Month 6 Tangled Hairball This represents the central operational dilemma of software architecture: drawing boundaries on a whiteboard is trivial, but keeping them from eroding under sprint pressure is where engineering actually occurs. --- The Running Case Study: The Entangled Fintech Engine To observe how boundaries erode and how to restore them, consider a production fintech payment engine. Financial systems operate under strict regulatory and mathematical constraints, making them ideal for studying boundary decay. fintech-engine/ └── src/ ├── auth/ │ ├── token.ts # Directly queries transaction tables for fraud flags │ └── user.ts ├── transactions/ │ ├── processor.ts # Imports ledger directly; bypasses immutability invariants │ └── gateway.ts # Talks to Stripe/Adyen ├── ledger/ │ ├── entry.ts # Imports webhook dispatcher inside atomic transactions │ └── account.ts ├── webhooks/ │ ├── dispatcher.ts # Synchronously invoked by ledger; network timeouts crash payments │ └── payload.ts └── shared/ └── db.ts # Shared global connection pool exposing raw SQL execution !Diagram 11: Entangled Dependencies and Shared Database Pool in the Fintech Engine This system must protect an inviolable architectural invariant: the double-entry balance invariant. Across all ledger accounts, the sum of all debits must strictly equal the sum of all credits at all times. No external service—whether an authentication handler, an external payment gateway adaptor, or a webhook dispatcher—may ever directly write to, bypass, or mutate ledger balance records. In the unsegregated codebase, that invariant is compromised daily. Novice engineering teams routinely treat account balances as mutable state, issuing destructive updates like UPDATE accounts SET balance = balance + 100. In high-throughput environments, this design pattern guarantees row lock contention, deadlocks, and silent reconciliation discrepancies. In contrast, industry benchmark financial platforms—such as Stripe's immutable ledger infrastructure handling five billion events per day—treat money movement strictly as an append-only event stream of balanced debit and credit entries. Balances are derived projections rather than mutable database columns, completely decoupling the financial source of truth from transient application state. The first time I saw this pattern in production, the circular dependency wasn't obvious from the code — it was obvious from the deployment. Changing the ledger required redeploying the auth service, which required redeploying the webhook dispatcher. Nobody could explain why anymore. In our entangled fintech engine, when a payment arrives, processor.ts opens a database transaction, calls gateway.ts to charge a credit card, directly executes an SQL update against the ledger accounts table, and immediately invokes dispatcher.ts to send a webhook to the merchant. If the merchant's webhook endpoint times out or returns a 500 error, the entire database transaction rolls back, undoing the ledger entry even though the customer's credit card was already charged. The business logic of the ledger was held hostage by an unreliable external network call because no architectural boundary isolated their failure domains. --- The Mathematics of Boundaries: Coupling Metrics That End Subjective Debates Architectural reviews frequently devolve into subjective debates where senior engineers argue personal preferences regarding clean code. To eliminate subjectivity, architects rely on the package coupling metrics formalized by Robert C. Martin. !Diagram 12: Afferent Coupling (Ca) vs. Efferent Coupling (Ce) in the Fintech Engine Afferent coupling, denoted as $C_a$, measures the number of classes or modules outside a package that depend upon classes inside it. A high $C_a$ indicates high responsibility: many parts of the application rely on this module, meaning any breaking changes to its public interface will reverberate across the system. Efferent coupling, denoted as $C_e$, measures the number of classes outside a package that classes inside this package depend upon. A high $C_e$ indicates high dependency: the module relies on numerous external abstractions and is vulnerable to breaking whenever its dependencies change. From these two metrics, Robert C. Martin's instability metric detailed in Clean Architecture derives the Instability Metric, denoted as $I$: $$I = \frac{C_e}{C_a + C_e}$$ The Instability metric ranges from zero to one. When $I = 0$, the module is maximally stable: it has zero outgoing dependencies ($C_e = 0$) and many incoming dependencies ($C_a > 0$), making it difficult and expensive to change because many systems rely on it. When $I = 1$, the module is maximally unstable: it has zero incoming dependencies ($C_a = 0$) and multiple outgoing dependencies ($C_e > 0$), making it volatile, easy to change, and dependent on external stability. Applying this formula to our fintech engine reveals exactly where boundaries must be drawn. The ledger/core domain should be designed with $I \approx 0$: nothing in the ledger should depend on authentication, payment gateways, or webhooks. Conversely, webhooks/dispatcher should operate with $I \approx 1$: it exists at the volatile edge of the system, consuming domain events and orchestrating outbound HTTP requests. This leads directly to the Stable Dependencies Principle (SDP): depend in the direction of stability. A module should only depend on modules that are more stable than itself. If a stable module like ledger with $I = 0.1$ imports an unstable module like webhooks with $I = 0.9$, the instability of the webhook package infects the ledger, destroying its stability and introducing unpredictable regression cascades. !Diagram 13: Robert C. Martin's Instability vs. Abstractness Health Map and The Main Sequence When a module is maximally stable ($I = 0$), it must also be abstract; otherwise, it becomes rigid and unmaintainable. Conversely, modules that are highly concrete should remain unstable ($I = 1$), allowing them to be modified rapidly without breaking external consumers. Calculating the distance from the Main Sequence, defined as $D = |A + I - 1|$ where $A$ represents abstractness, provides an objective mathematical index of architectural health that eliminates opinion from pull request reviews. --- Making Boundaries Irreversible: Language-Level and Tooling Enforcement Conventions documented in internal wikis or discussed during architecture offsites will always collapse under the pressure of production deadlines. If the compiler, the language runtime, or the continuous integration pipeline allows an illegal cross-boundary import, an engineer working under sprint duress will eventually merge it. Sustainable architecture requires mechanical enforcement. A landmark demonstration of mechanical boundary enforcement occurred at Shopify between 2019 and 2022. Shopify operated a massive Ruby on Rails monolith containing over 2.8 million lines of code. Over a decade of hypergrowth, coupling between disparate parts of the monolith became so severe that test suites took hours to run, boot times crippled developer workstations, and unexpected regressions occurred continuously. Instead of paying the enormous operational tax of splitting into dozens of microservices, Shopify built and open-sourced Packwerk in September 2020. Packwerk runs static analysis on Ruby abstract syntax trees during CI builds, enforcing two boundaries separately: package privacy (preventing external packages from referencing internal constants directly) and explicit dependency declarations (forbidding undeclared inter-package couplings). In their honest retrospective, Shopify acknowledged that Packwerk is "a sharp knife"—at points they even evaluated removing it due to developer friction and ongoing maintenance overhead. Yet the structural value was undeniable: > "Packwerk has provided value in holding the line against new dependencies at the base layer of our application." By turning module boundaries into automated build gates, Shopify significantly reduced unexpected regressions, cut test suite times, and maintained the operational simplicity of a single deployable application — without the coordination overhead of splitting into dozens of microservices. Modern ecosystems provide distinct mechanisms to make boundaries irreversible: !Diagram 14: The Mechanical Boundary Enforcement Hierarchy — From Compiler Gates to Linters In the Go toolchain, internal/ packages enforce privacy directly through the compiler, terminating builds if external packages attempt unauthorized imports. In Rust, pub(crate) and hierarchical module scoping provide compiler-level guarantees that private domain logic cannot be accessed beyond designated crate or module boundaries. In the Node.js and TypeScript ecosystem, modern packages utilize the exports map in package.json alongside subpath imports to declare public entry points while marking internal implementations unresolvable to external consumers. Furthermore, monorepos running Nx, Turborepo, or TypeScript Project References use tools like eslint-plugin-boundaries to enforce boundary rules in CI, failing builds if presentation layers import persistence databases directly. In the Java and Kotlin ecosystems, architects achieve equivalent mechanical enforcement using ArchUnit. ArchUnit allows engineers to write unit tests that inspect compiled Java bytecode, verifying architectural rules in compiled bytecode as automated assertions: @Test public void ledgerShouldNotDependOnWebhooks() { noClasses() .that().resideInAPackage("..ledger..") .should().dependOnClassesThat().resideInAPackage("..webhooks..") .check(importedClasses); } If a developer introduces an illegal import between ledger and webhooks, the build breaks during standard unit testing, preventing the violation from ever reaching a shared branch. Stripe implemented a parallel pattern across their monolithic Ruby codebases, developing the Sorbet static type checker to enforce strict directional dependency graphs and prevent payment processing logic from coupling to customer billing models. These automated boundary checks represent concrete implementations of Architectural Fitness Functions, an engineering discipline formalized by Neal Ford, Rebecca Parsons, and Patrick Kua in Building Evolutionary Architectures. Just as unit test suites continuously verify that business calculations produce correct numerical output, architectural fitness functions provide continuous, automated verification that structural invariants—such as package instability, directional dependencies, and blast-radius isolation—do not degrade under deadline pressure or unvetted AI-generated PR volume. --- The Architecture of Intent Before an engineer ever traces an execution thread through a database transaction or measures the throughput of an asynchronous message queue, the architecture of the system has already communicated its intent. The macroscopic geography of the repository speaks through its root contracts: its immutable migration ledgers, its isolated infrastructure blast radii, its automated task runners, its regulatory code ownership, and its defensive pre-commit firewalls. When you step inside src/, that same architectural discipline transforms into boundary enforcement. By replacing subjective debates with mathematical coupling metrics, identifying core domain invariants like the double-entry balance rule, and backing those boundaries with compiler and CI gates, teams insulate their systems against sprint fatigue, deadline shortcuts, and unchecked AI-generated code sprawl. The boundary is drawn. The enforcement is in place. But boundaries are not static — they are stress-tested every time the team grows. In Part 3, we map the universal root of the fintech engine — every folder, every governance file, every security contract — and the precise moment each one became necessary. Before we watch the system grow, we need to understand exactly what we built.