Skip to content

Enforce the configured git identity on every commit#72

Merged
brycelelbach merged 1 commit into
brycelelbach:mainfrom
robobryce:add/git-identity-enforcement
Jun 7, 2026
Merged

Enforce the configured git identity on every commit#72
brycelelbach merged 1 commit into
brycelelbach:mainfrom
robobryce:add/git-identity-enforcement

Conversation

@robobryce

Copy link
Copy Markdown
Contributor

What & why

The bootstrap configures a global git author, email, and (optionally) a signing key, but unattended agents constantly ignore it — they commit under their own identity via git -c user.email=..., git commit --author=..., GIT_AUTHOR_* / GIT_COMMITTER_* env vars, or a repo-local git config user.email. This adds two layers that keep commits on the configured identity:

(0) An agent rule in every harness's global instruction file. write_agent_git_rules writes a managed block to ~/.claude/CLAUDE.md (Claude Code user memory, loaded in every repo) and ~/.codex/AGENTS.md (Codex global instructions, loaded from CODEX_HOME in every repo), telling the agent to always commit with the configured identity and leave the global git config alone. The block uses the standard # >>> autonomous-agent-bootstrap >>> markers, so re-runs replace it in place and any pre-existing content in those files is preserved.

(1) A global git hook that enforces it. install_git_hooks writes a dispatcher to ~/.aab/git-hooks/aab-git-hook, symlinks it under each managed hook name, and points the global core.hooksPath at that directory. On pre-commit the dispatcher:

  • reads the expected identity from git config --global --get user.name/user.email — immune to -c, GIT_CONFIG_PARAMETERS, env, and repo-local overrides;
  • reads the actual identity from git var GIT_AUTHOR_IDENT / GIT_COMMITTER_IDENT — which do reflect --author and GIT_AUTHOR_*/GIT_COMMITTER_* env vars that the effective git config user.email does not;
  • rejects the commit on any mismatch of author or committer name/email;
  • when global signing is on (commit.gpgsign=true), also rejects commits that disable signing via config (-c commit.gpgsign=false) or swap the signing key.

Because a global core.hooksPath replaces rather than supplements a repo's .git/hooks, the dispatcher chains through to the repository's own hook of the same name (located via --git-common-dir, so worktrees and the core.hooksPath self-reference are handled) after its checks pass — projects that ship Husky / pre-commit / lint-staged hooks keep working.

Scope is intentionally limited to identity and signing. The deliberate per-commit escape hatches git provides — git commit --no-verify (skips hooks) and the --no-gpg-sign flag (skips signing) — are left intact. When no global identity is configured, the hook is a no-op and all commits pass.

Design notes

  • The dispatcher is emitted from bootstrap.bash via emit_git_hook_script (a quoted heredoc) so the script stays self-contained for the curl ... | bash install path. test.bash --lint extracts and shellchecks the emitted hook separately, since shellcheck can't see inside the heredoc.
  • --git-path hooks is deliberately not used to find the repo-local hook: it honors core.hooksPath and would resolve back to the dispatcher (infinite self-exec). --git-common-dir/hooks is used instead, plus a symlink-identity guard.

Test plan

  • ./test.bash (lint + unit)

    === lint ===
    === unit (bats) ===
    1..108
    ...
    ok 107 load_config_stdin: empty stdin is a silent no-op
    ok 108 main() runs load_config_file only when given a positional arg (unset env vars populated)
    

    108/108 pass. (The trailing BW01 advisory is from the pre-existing load_config_file aborts on malformed input test, unrelated to this change.) 17 new unit tests cover hook install + symlinks + core.hooksPath, emitted-hook validity, idempotency, the full identity-override matrix (-c, --author, GIT_AUTHOR_EMAIL, GIT_COMMITTER_EMAIL, repo-local config), --no-verify pass-through, the no-global-identity no-op, repo-hook chaining (pass + fail), signing-disabled block, and the rule-file managed block (write / idempotent / preserve).

  • ./test.bash --docker (full e2e in a fresh ubuntu:22.04 container)

    Run from the main checkout (not a worktree), GITHUB_TOKEN forwarded. Exit 0. New assertions (12b/12c/12d) passed in both idempotency runs:

    [bootstrap] Installed global git hooks at /root/.aab/git-hooks and set core.hooksPath (enforces the global commit identity).
    [bootstrap] Wrote git-identity rule to /root/.claude/CLAUDE.md.
    [bootstrap] Wrote git-identity rule to /root/.codex/AGENTS.md.
    ...
    PASS: git identity enforcement hook installed and wired via core.hooksPath.
    PASS: git hook allows the configured identity and blocks overrides.
    PASS: agent instruction files carry the git-identity rule exactly once.
    ...
    All e2e assertions passed.
    All e2e assertions passed.
    === e2e passed ===
    === docker e2e passed ===
    
  • ./test.bash --secrets (gitleaks v8.18.4)

    63 commits scanned.
    no leaks found        (full-history scan)
    no leaks found        (--no-git working-tree scan)
    
  • ./test.bash --e2e — covered by --docker above, which runs --e2e inside the container against a pristine $HOME. Not run a second time directly on this host to avoid mutating the host's real ~/.gitconfig / core.hooksPath.

  • ./test.bash --smoke — N/A for this change: it spends live inference and exercises provider wrappers, which this PR does not touch.

🤖 Generated with Claude Code

Agents routinely ignore the global git author, email, and signing key the
bootstrap configures, committing under their own identity via `git -c
user.email=...`, `git commit --author=...`, GIT_AUTHOR_*/GIT_COMMITTER_* env
vars, or a repo-local `git config user.email`. Add two layers that keep
commits on the configured identity.

write_agent_git_rules writes a managed block to each harness's global
instruction file — ~/.claude/CLAUDE.md (Claude Code) and ~/.codex/AGENTS.md
(Codex), both loaded in every repository — telling the agent to always commit
with the configured identity and leave the global git config alone.

install_git_hooks makes the rule non-optional. It writes a dispatcher to
~/.aab/git-hooks/aab-git-hook, symlinks it under each managed hook name, and
points the global core.hooksPath at it. On pre-commit the dispatcher compares
the resolved author and committer identity (git var GIT_AUTHOR_IDENT /
GIT_COMMITTER_IDENT — which reflect --author and the env-var overrides that
the effective `git config user.email` does not) against the global user.name /
user.email (read from --global, which -c / env / repo-local config cannot
poison) and rejects mismatches. When global signing is configured it also
rejects commits that disable signing via config or swap the signing key. The
dispatcher chains through to the repository's own hook of the same name,
because a global core.hooksPath replaces rather than supplements .git/hooks.

The deliberate per-commit escape hatches git provides (--no-verify, the
--no-gpg-sign flag) are left intact, and the hook is a no-op when no global
identity is configured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brycelelbach brycelelbach merged commit 3be48ff into brycelelbach:main Jun 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants