From bd9eee8985fcf6e1f8237d3f3827a81b70fdf331 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 02:10:30 +0000 Subject: [PATCH] git: guard this repo's commits against a stray work identity Adds pre-commit, pre-merge-commit, and pre-push hooks that refuse any committer address outside a hard-coded two-address allowlist: the personal identity and the one Claude Code web sessions commit under. ./install wires them up by pointing this clone's core.hooksPath at git/hooks (a relative path resolves against the repo root), so the guard is repo-local -- no other repo on the machine is affected. The check is on the committer, not the author: the committer is whoever is running git right now, which is the identity that leaks, while the author is provenance worth preserving when amending or rebasing someone else's work. pre-push is a backstop for commits that never saw pre-commit -- rebase, cherry-pick, --no-verify, or predating the hooks. All of it is bypassable by design; enforcement that cannot be bypassed has to live on the remote. --- CLAUDE.md | 8 +++++ README.md | 4 ++- git/hooks/identity-guard.sh | 61 +++++++++++++++++++++++++++++++++++++ git/hooks/pre-commit | 7 +++++ git/hooks/pre-merge-commit | 7 +++++ git/hooks/pre-push | 55 +++++++++++++++++++++++++++++++++ install.conf.yaml | 4 +++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 git/hooks/identity-guard.sh create mode 100755 git/hooks/pre-commit create mode 100755 git/hooks/pre-merge-commit create mode 100755 git/hooks/pre-push diff --git a/CLAUDE.md b/CLAUDE.md index 4ab2e1a..624f4f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,3 +107,11 @@ split applies to config that isn't a sidecar: private work-machine bspwm profiles live in the overlay, while intentionally public hardware-specific profiles may live in `desktop-environment/bspwm/profiles/`. + +`git/hooks/` guards this repo's own history: `./install` points this clone's +`core.hooksPath` at it (repo-local — no other repo on the machine is +affected), and the hooks then refuse any commit or push whose committer is +not one of the two identities this repo is developed under. The allowlist in +`identity-guard.sh` is deliberately hard-coded and closed; never add a work +or otherwise private address to it — this repo is public, so anything +written there is published. diff --git a/README.md b/README.md index c3982af..cd7a6fc 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ each gets its own top-level directory: - `bash/` — `bashrc`, `bash_aliases`, `profile`, `inputrc`, `bazel_completions.bash` -- `git/` — `gitconfig`, `gitmessage`, `git-prompt.sh`, `lazygit.yml` +- `git/` — `gitconfig`, `gitmessage`, `git-prompt.sh`, `lazygit.yml`, + `hooks/` (committer-identity guard for this repo, wired up by `./install` + via a repo-local `core.hooksPath`) - `tmux/` — `tmux.conf`, `settings.conf`, plugins via submodules (tpm, nord-tmux, tmux-sensible) - `kitty/` — config, themes, `launch.sh`, `zenmode.py` diff --git a/git/hooks/identity-guard.sh b/git/hooks/identity-guard.sh new file mode 100644 index 0000000..caffb46 --- /dev/null +++ b/git/hooks/identity-guard.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Shared logic for the identity-guarding hooks in this directory. Sourced, +# never run directly (git only executes files named after a hook). +# +# These hooks apply to this repo only: ./install points this clone's +# core.hooksPath at git/hooks, and no other repo is touched. The guard exists +# to stop a work identity from ever committing here. It is a guard against +# habit, not against an attacker: --no-verify, `git -c core.hooksPath=`, and +# editing this file all bypass it. Enforcement that cannot be bypassed has to +# live on the remote. + +# The only committer addresses allowed to create commits in this repo: +# the personal identity and the one Claude Code web sessions commit under. +guard_allowed_emails() { + printf '%s\n' 'krishna0bala@proton.me' 'noreply@anthropic.com' +} + +# Lowercase, since the domain half of an address is case-insensitive and the +# local half is in practice. +guard_normalize() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' +} + +# The address inside a "Name 1730000000 +0000" ident string. +guard_ident_email() { + _ident=${1#*<} + guard_normalize "${_ident%%>*}" +} + +guard_email_allowed() { + _candidate=$(guard_normalize "$1") + for _allowed in $(guard_allowed_emails); do + if [ "$_candidate" = "$_allowed" ]; then + return 0 + fi + done + return 1 +} + +# Reject the commit about to be created if it would carry an unlisted +# committer. The committer, not the author, is the identity being checked: +# the committer is whoever is running git right now, which is the thing that +# leaks, while the author is provenance that gets preserved when you amend or +# rebase someone else's work. +guard_check_committer() { + _email=$(guard_ident_email "$(git var GIT_COMMITTER_IDENT)") + if guard_email_allowed "$_email"; then + return 0 + fi + + cat >&2 <. + +Committers allowed in this repo: +$(guard_allowed_emails | sed 's/^/ /') + +Set the right identity for this repo: + git config user.email
+EOF + return 1 +} diff --git a/git/hooks/pre-commit b/git/hooks/pre-commit new file mode 100755 index 0000000..1de5197 --- /dev/null +++ b/git/hooks/pre-commit @@ -0,0 +1,7 @@ +#!/bin/sh +# Runs on `git commit` and `git commit --amend`. +set -eu + +. "$(dirname "$0")/identity-guard.sh" + +guard_check_committer diff --git a/git/hooks/pre-merge-commit b/git/hooks/pre-merge-commit new file mode 100755 index 0000000..6bbe8ea --- /dev/null +++ b/git/hooks/pre-merge-commit @@ -0,0 +1,7 @@ +#!/bin/sh +# Runs on `git merge` when it creates a merge commit; pre-commit does not. +set -eu + +. "$(dirname "$0")/identity-guard.sh" + +guard_check_committer diff --git a/git/hooks/pre-push b/git/hooks/pre-push new file mode 100755 index 0000000..9162523 --- /dev/null +++ b/git/hooks/pre-push @@ -0,0 +1,55 @@ +#!/bin/sh +# Backstop for commits that never passed pre-commit: rebase, cherry-pick, +# `git commit --no-verify`, and anything committed before this hook existed. +set -eu + +. "$(dirname "$0")/identity-guard.sh" + +zero=$(git hash-object --stdin " line per ref +# being pushed. Collect the local tips; a zero local sha is a branch deletion, +# which pushes no commits. +tips='' +while read -r _local_ref local_sha _remote_ref _remote_sha; do + if [ -n "$local_sha" ] && [ "$local_sha" != "$zero" ]; then + tips="$tips $local_sha" + fi +done + +if [ -z "$tips" ]; then + exit 0 +fi + +# Only commits not already reachable from a remote-tracking ref: the ones this +# push would actually add. Checking the committer rather than the author keeps +# this quiet on replayed outside work — cherry-pick and rebase re-stamp the +# committer as you while preserving the original author. +offenders='' +count=0 +for line in $(git log --format='%h=%ce' $tips --not --remotes); do + if ! guard_email_allowed "${line#*=}"; then + count=$((count + 1)) + if [ "$count" -le 10 ]; then + offenders="$offenders ${line%%=*} committed by <${line#*=}> +" + fi + fi +done + +if [ "$count" -eq 0 ]; then + exit 0 +fi + +cat >&2 < + git commit --amend --reset-author --no-edit +EOF +exit 1 diff --git a/install.conf.yaml b/install.conf.yaml index 359ed2e..8ee4b0e 100644 --- a/install.conf.yaml +++ b/install.conf.yaml @@ -29,3 +29,7 @@ - shell: - [git submodule update --init --recursive, Installing/updating submodules] + # Repo-local only: points this clone's hooks at the tracked git/hooks + # (relative paths resolve against the repo root). Other repos on the + # machine are untouched. + - [git config core.hooksPath git/hooks, Enabling identity-guard hooks for this repo]