1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

`.gitignore` では履歴から消えない ― リポジトリを履歴ごと public にする前と後にやった 7 つのこと ― 架空サカスタのチケットサイトで学ぶ(番外 2)

1
Posted at

.gitignore では履歴から消えない ― リポジトリを履歴ごと public にする前と後にやった 7 つのこと ― 架空サカスタのチケットサイトで学ぶ(番外 2)

はじめに

架空のサッカースタジアム「ペヌソニックスタジアム(通称ペヌスタ)」のチケット販売サイトを、遊び場として作っています。

ゴール裏年パス勢が、架空スタジアムのチケットサイトを作る理由 ― 架空サカスタのチケットサイトで学ぶ(0)
https://qiita.com/macchi23/items/5b9f5670c841f5c41be8

このリポジトリは 2026-10-14 に GitHub で public にしました。公開した時点のコミットは 88 件、今は 134 件です(2026-11-07 時点)。

ポートフォリオとして出したかったからです。仕事で書いたコードは外に出せないので、自分の手で作ったものを見せる場所がほしかった。

新しいリポジトリに最新の状態を 1 コミットで出す手もありましたが、履歴ごと出すことにしました。
1 ステップずつのコミットと、運用の台帳が育っていく様子も含めて見せたかったからです(台帳の話は番外 1 に書きました)。

履歴ごと出すと、過去のコミットに残っているものも全部見えます。今のファイルから消しただけでは足りません。
この記事では、公開の前にやった 4 つと、公開したあとに足した 3 つを書きます。

日付 やったこと
10/13 作業ツリーと全履歴の棚卸し
10/13 LICENSE は付けないと決めた
10/14 作者のメールアドレスを全履歴で noreply に書き換え
10/14 private で作って push → 中身を見て、構成メモを履歴ごと消す → public に切り替え
10/18 Secret Scanning が無効だったのに気づいて有効化
10/18 gitleaks をコミット時と check に入れる
10/18 未公開の下書きを別の private リポジトリに分ける

全履歴の棚卸し: 何を探したか

まず、作業ツリーと全履歴の両方を grep しました。探したのは次のものです。

  • 追跡しているファイル: .env 系、鍵ファイル、CDK のコンテキストファイル、Claude Code のローカル設定、出力物やダンプ
  • 内容: 12 桁の数字(AWS アカウント ID)、arn:aws、AKIA(アクセスキー)、APP_KEY=base64:、メールアドレス、/Users/ を含むパス、バケット名、SSO の URL、個人名
  • 全履歴: 上と同じパターンを、削除済みのファイルも含めて
  • 作者とコミッターのメールアドレスの一覧
  • docs の中の個人的な内容、実在の組織名

最初の棚卸しでは、git log --all -p -G'<パターン>' で、パターンを含む差分だけを出しました。
全コミットのファイルを総当たりするなら、git grep に全リビジョンを渡します(公開前のレビューで使った形です)。

git grep -I -n -E '(AKIA|ASIA)[0-9A-Z]{16}|arn:aws' $(git rev-list --all) \
  -- ':!**/package-lock.json' ':!**/composer.lock'

結果は、機密値が 0 件でした。.env は過去にも一度も追跡されておらず、/Users/ を含むパスも全履歴で 0 件です。
対処が要ったのは、次の 2 つでした。

  • 作者のメールアドレス: 全コミットが普段使いのアドレスだった
  • 個人的な docs: 構想メモにあった業務に関する一文

業務に関する一文は、最新のファイルから消しました。特定できる内容ではなかったので、履歴の書き換えまではしていません(消したコミットのメッセージにも「会社の話を消す」と残っています)。

ローカル用のダミーの認証情報(開発用の DB と S3 互換ストレージのもの)は、公開してよいと判断しました。

公開の直前に、もう一度別の役(セキュリティのレビュー)に全履歴を見てもらいました。本名、メールアドレス、ホームのユーザー名がコミットメッセージにも残っていないか、デモ用のログインが本番で閉じるか、といった観点です。
判定は「公開してよい(必須の対処なし)」でした。12 桁の数字に当たったのは、テストのダミーの UUID(...-000000000000)だけです。

作者のメールアドレスを、全履歴で noreply に書き換える

git のコミットには、作者のメールアドレスが入ります。public にすると、誰でも git log で読めます。

普段使いのアドレスは、ほかのサービスのログインにも使っているので、公開の場に置きたくありませんでした。

GitHub には、アドレスの代わりに使える noreply のアドレス(2017 年 7 月以降に作ったアカウントなら ID+ユーザー名@users.noreply.github.com)があります。
全履歴の作者とコミッターを、これに書き換えました。使ったのは git filter-repo です。

mailmap.txt
macchi23 <ID+macchi23@users.noreply.github.com> 旧い名前 <旧いアドレス>
git config user.name macchi23                       # これからのコミット用(このリポジトリだけ)
git config user.email ID+macchi23@users.noreply.github.com
git clone --mirror . ../penusta-backup.git          # 書き換え前のバックアップ
git filter-repo --mailmap mailmap.txt --force

全 85 コミットが noreply に揃い、かかった時間は 0.34 秒でした。
git log --all --format='%an <%ae> | %cn <%ce>' | sort | uniq -c で全コミットが noreply になったこと、git log --all -p に旧いアドレスと本名が 0 件なことを確かめました。

いくつか知っておくとよいことがあります。

  • 先にバックアップを取る: filter-repo は、新しく clone したリポジトリでないと --force を求めます。このチェックが「バックアップはあるか」の代わりで、filter-repo は既定で reflog と古いオブジェクトをすぐ消します。バックアップは本名とアドレスが残っているので、確認が済んでから消しました
  • コミット ID は全部変わる: まだ一度も push していなかったので、実害はありませんでした。docs の中でコミット ID を参照している箇所が無いことも確かめました。コミットメッセージの中の短い ID は、filter-repo が新しい ID に書き換えます
  • GitHub 側の設定は、最新のコミットしか見ない: Settings → Emails の「Block command line pushes that expose my email」は、push のたびに最新のコミットだけを見ます。過去のコミットには効かないので、履歴の書き換えが要りました

自分のアカウントでは、「Keep my email addresses private」と「Block command line pushes that expose my email」の両方を有効にしています。

.gitignore では消えない: 見せたくないファイルを履歴ごと消す

GitHub にはまず private で作って push し、ブラウザで中身を見てから public に切り替えました。
中身を確かめる観点を Claude Code に挙げてもらったところ、その中に連載の構成メモ(まだ書いていない回の題材と予定)が入っていました。

構成メモは自分用のメモで、書きかけの判断や予定の日付が並んでいます。人に見せる形にはなっていません。

最初は .gitignore に入れればいいと思いましたが、それでは足りません。理由は 2 つです。

  • .gitignore は、追跡していないファイルにしか効かない
  • 追跡を外しても、過去の 4 コミット分の中身が Commits の画面から読める

そこで、ファイルを退避してから履歴ごと消し、戻して .gitignore に入れました。

git filter-repo --invert-paths --path docs/article-series.md --force

このファイルに触れていた 4 コミットは、どれも他のファイルも変えていたので、空になって消えたコミットはありません(87 件のまま)。
GitHub には force push しました。

force push で上書きした古いコミットは、SHA を直接指定すれば見られることがあります。GitHub の docs にも、書き換えて force push しても、キャッシュされた表示や fork には残りうると書いてあります。

リポジトリを消して作り直す案もありましたが、やめました。
まだ private の間で、古いコミットの SHA を知っているのは手元のログだけです。中身も予定で、秘密情報ではありません。必要になれば GitHub のサポートに頼む手も残っています。

LICENSE は付けない

このリポジトリは見てもらうために出すもので、使ってもらうためではありません。迷うなら付けない側にしておいて、必要になったら足せばいいと考えました。

ライセンスが無いと、通常の著作権が適用され、権利はすべて作者に残ります。
ただし GitHub の利用規約で、public にしたリポジトリは、GitHub の上で見ることと fork することを他のユーザーに許すことになっています。

LICENSE なし MIT
GitHub の上で見る・fork する できる(利用規約) できる
手元に持ち出して使う・改変する・配る 許していない 条件付きでできる

README の末尾にも「ライセンス」の節を作り、「付けていない。閲覧は自由だが、コードの再利用・再配布は許可していない。」と書きました。

公開後に気づいた: Secret Scanning が無効だった

公開から 3 日後(10/18)、gitleaks(秘密情報の検査ツール)を入れるか検討していたとき、ついでにリポジトリの設定を API で見ました。

gh api repos/<owner>/<repo> --jq .security_and_analysis

Secret Scanning(push されたものから秘密情報を探してアラートを出す機能)と、リポジトリ単位の Push Protection(秘密情報を含む push を止める機能)が、両方とも無効になっていました。
(public リポジトリでは、この設定とは別に、AWS などのキーの形式は GitHub が既定で検査して提供元に知らせます。無効だったのは、自分にアラートが届く方です)
すぐに有効にしました。

gh api -X PATCH repos/<owner>/<repo> \
  -f 'security_and_analysis[secret_scanning][status]=enabled' \
  -f 'security_and_analysis[secret_scanning_push_protection][status]=enabled'

公開前にあれだけ確かめたのに、GitHub の側の設定は見ていませんでした。確かめる範囲が、リポジトリの中だけになっていました。

GitHub は 2024 年 3 月から、個人アカウントで新しく作る public リポジトリでは、この 2 つを既定で有効にしています。

このリポジトリは private で作って、15 分後に public に切り替えました。「新しく作る public リポジトリ」に当たらなかったのが原因だと推測していますが、private から public への切り替えについて書いた公式の情報は見つけられていません。
もし推測どおりなら、「まず private で push して中身を見る」という安全側の手順が、無効のままになった原因です。

Push Protection には 2 種類あります。

ユーザー単位 リポジトリ単位
既定 有効 無効(個人アカウントで新しく作る public リポジトリは作成時に有効)
何を止めるか 自分のアカウントから public リポジトリへの push そのリポジトリへの push
すり抜けたとき リポジトリ単位が無効なら、アラートは残らない アラートと監査ログが残る

自分のアカウントの「Push protection for yourself」は、今見ると有効になっていました(触った覚えはないので既定のまま)。なので、リポジトリ単位が無効だった 3 日あまりも、自分の push は止まる状態だったはずです。

今のアラートは 0 件です。

gitleaks はコミット時と check の 2 か所に

GitHub が止めるのは push の時点で、そのときにはもうコミットされています。
直すには履歴の書き換えが要り、公開の前にやった作業をもう一度やることになります。手元で回せば、コミットの前に止められます。

秘密情報は一度 public に出たら取り消せません。1 回漏れてから入れるのでは遅いと考えました。

運用の台帳では「同じことが 2 回起きたら仕組みにする」決まりですが、これは例外として、発生 0 回のまま入れました(番外 1 で書いた例外です)。

入れた場所は 2 か所です。

場所 いつ 何を見るか 役割
pre-commit フック(scripts/git-hooks/pre-commit) git commit のたび(人でも Claude Code でも) ステージした差分 本命。コミットされる前に止める
scripts/check.sh secrets 検証のたび 全履歴と、別リポジトリ(docs/private、後述)の今のファイル フックの取りこぼしを拾う

フックの取りこぼしは、たとえば次のものです。

  • --no-verify で通したコミット、フックを有効にしていない環境
  • フックを足す前のコミットに戻って作業したとき。フックはリポジトリの中のファイルを指しているので、そのファイルが無いと git は黙ってフックを飛ばす
  • pre-commit は git commit からしか呼ばれない。衝突しない merge、cherry-pick、rebase で作られるコミットは見ない

push の時点の GitHub と合わせて、3 か所で止まる形です。

フックは git config core.hooksPath scripts/git-hooks で有効にします。ローカルの設定なので、clone した人には効きません。

設定ファイルでは、gitleaks の既定のルールに 1 本だけ足しています。

.gitleaks.toml
[extend]
useDefault = true

[[rules]]
id = "aws-account-id-in-arn"
description = "ARN に含まれる AWS アカウント ID"
regex = '''arn:aws[a-z-]*:[a-z0-9-]+:[a-z0-9-]*:[0-9]{12}:'''
keywords = ["arn:aws"]

  # AWS ドキュメントの例示 ID
  [[rules.allowlists]]
  description = "AWS ドキュメントの例示アカウント ID"
  regexTarget = "match"
  regexes = ['''arn:aws[a-z-]*:[a-z0-9-]+:[a-z0-9-]*:(123456789012|111122223333|444455556666|777788889999):''']

AWS アカウント ID は、記事でも伏せると決めているものです。既定のルールには、アカウント ID を拾うものがありません。
ただ、12 桁の数字そのものを拾うと、UUID やイメージのダイジェスト、lock ファイルのハッシュの一部にも当たります(このリポジトリでも、テストのダミー UUID などに当たりました)。なので、ARN の中に入っている形だけを拾います。AWS のドキュメントに出てくる例示の ID は許可しています。

フックの中身はこうです。

scripts/git-hooks/pre-commit(抜粋)
# 先頭で root="$(git rev-parse --show-toplevel)" に cd し、gitleaks があるか確かめている
out="$(GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=color.diff GIT_CONFIG_VALUE_0=never \
  gitleaks git --pre-commit --staged -v --redact --no-banner --no-color --exit-code 99 \
  --config "$root/.gitleaks.toml" "$root" 2>&1)"
rc=$?
[ -n "$out" ] && printf '%s\n' "$out" >&2

if [ "$rc" -eq 99 ]; then
  echo "秘密情報の疑いがあるのでコミットを止めた。値を消してから再コミットする。誤検知なら .gitleaks.toml で許可する" >&2
  exit 1
elif [ "$rc" -ne 0 ]; then
  echo "gitleaks の実行に失敗(rc=${rc})。コミットを止める。急ぐときは git commit --no-verify" >&2
  exit 1
fi
# (以下、出力に ERR があれば止める)

細かいところで、いくつか罠がありました。

  • 漏れの検出と実行の失敗を分ける: --exit-code 99 にして、99 なら漏れ、それ以外の非 0 なら実行の失敗として扱う。どちらも止める
  • 終了コードが 0 でも失敗していることがある: git の呼び出しに失敗しても、gitleaks は終了コード 0 で「no leaks found」を返すことがある。出力に ERR があれば止める
  • 差分の色で素通りする: git の設定で color.ui=always にしていると、差分に色のコードが混ざって検出できない。環境変数で color.diff=never を強制している
  • 漏れたときに、すり抜け方を案内しない: 漏れを検出したときのメッセージには --no-verify を書かない。本物の漏れのときに、通し方を教えることになるため

最初は Docker の中で動かす作りでしたが、git commit -a と git の worktree からのコミットで、秘密情報がそのまま通りました。ホストで動かす作りに変えています。
見つけた経緯は番外 1 に書きました。作り直した版は、commit -a、--amend、merge の衝突を解いた後、サブディレクトリから、worktree など 9 つの経路で止まることを確かめています。

Claude Code をサブエージェントで分業させ、仕組みは台帳で育てる ― 架空サカスタのチケットサイトで学ぶ(番外 1)
https://qiita.com/macchi23/items/f321a699d61b38cd7636

今、scripts/check.sh secrets は全履歴 134 コミット(約 2.3MB)を約 0.5 秒で見て、検出は 0 件です。

未公開の下書きは private リポジトリに分ける

構成メモを履歴から消したあとも、記事の下書きをどこに置くかは 2 回変えました。

  1. 10/14: 構成メモの 1 ファイルだけを .gitignore に入れた
  2. 10/15: 未公開の下書きも、.gitignore したフォルダ(docs/articles/drafts/)に置くようにした
  3. 10/18: 記事まわりを丸ごと docs/private/ に移し、その中を別の private リポジトリにした

2 のときは、push しないブランチに置く案と、別の private リポジトリに分ける案とも比べました。
ブランチは git push --all などで誤って出てしまう道が残ります。別リポジトリは記事 1 本のためには重いと考え、.gitignore したフォルダにしました。

3 に変えたのは、.gitignore したファイルには履歴もバックアップも無く、Mac が壊れたら消えるからです。
公開済みの回も、正は Qiita の方です。public 側に写しを置く意味は薄いので、記事まわりは全部 private 側に置くことにしました。

きっかけは覚えていませんが、「リポジトリって入れ子にできるの?」と思ったのが始まりです。やったこともなく、できるかどうかも知りませんでした。

置き方は、サブモジュールにせず、.gitignore したディレクトリの中にそのまま別のリポジトリを置く形にしました。

  • サブモジュールにすると、private リポジトリの URL が public 側に出る。clone した人はサブモジュールの中身を取れない(取ろうとすると権限エラーになる)
  • 隣のディレクトリに置くと、Claude Code の作業ディレクトリの外になり、記事の素材(コードや開発メモ)と行き来しにくい
.gitignore(抜粋)
# 公開しないドキュメント(記事の構成メモ・下書きなど)。中は別の private リポジトリ penusta-private
docs/private/

注意点は 2 つです。

  • .gitignore に入れてから、中で git init する。逆だと、親の git add -A で中のリポジトリが登録されかける
  • 親の git status には中の変更が出ない。中で別にコミットして push する

private リポジトリでは、GitHub の秘密情報の検知は使えません。ユーザー単位の Push Protection は public リポジトリへの push しか止めず、リポジトリ単位の Push Protection と Secret Scanning には GitHub Secret Protection(Team / Enterprise プランで買う有料機能)が要ります。
なので、private 側も同じ gitleaks のフックを使うようにし、親の check.sh secrets でも docs/private の中を検査しています。

残っている穴

  • private 側の履歴は検査していない: check.sh secrets が見るのは docs/private の今のファイルだけで、private リポジトリの過去のコミットは見ていない。private なので、実害は小さいと考えている
  • フックは clone した人には効かない: ローカルの設定なので。書き込めるのは自分だけなので、clone した人がこのリポジトリに直接 push することはない。ただし PR を受けると、その中身はこのリポジトリからも取得できる
  • 上書きした古いコミット: force push する前のコミットは、SHA を知っていれば見られる可能性が残る

まとめ

履歴ごと public にする前に確かめたこと:

  • 作業ツリーだけでなく、全履歴を grep した(git grep ... $(git rev-list --all))
  • 作者とコミッターのメールアドレスを確かめた(必要なら filter-repo --mailmap で noreply に)
  • 見せたくないファイルは、.gitignore ではなく履歴から消した(filter-repo --invert-paths)
  • LICENSE を付けるかどうかを決めて、README に書いた
  • public にしたあと、Secret Scanning と Push Protection が有効か API で確かめた
  • 秘密情報の検査を、コミットの前に入れた

public にする前の作業より、公開した後に足したものの方が多くなりました。private で作ってから public に切り替えるなら、GitHub の側の設定は自分で確かめた方がいいです。

リポジトリはこちらです。

シリーズの他の回:

番外編:

この記事の骨格は Claude Code に作らせ、自分の言葉で書き直しています。棚卸しや履歴の書き換えも Claude Code に任せ、結果(件数、設定の状態)は手元のリポジトリと GitHub の API で確かめています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?