Identity hook: a push with no refspec is read from git's configuration, and a git the command may redefine is refused

Merged
DO
3d ago
+850-165

Closes #111.

The identity hook refuses a commit, a push or a GitHub write that isn't the Maintainer's. Until now it read a push with no refspec as HEAD, and it trusted that git in the command was git. Now:

A push with no refspec, or with no remote, is read from git's configuration. One reader in identity.py, which both hooks call:

  • What it reads: the current branch's pushRemote, then remote.pushDefault, then the branch's own remote; and push.default, remote.*.push and remote.*.mirror.
  • How it reads them:
    • mirror is read as --mirror.
    • Configured refspecs are read as if given on the command line.
    • matching is read as --branches.
    • Every other push.default value is read as HEAD.
  • What it refuses: a wildcard, an unknown value, or config it cannot read.
  • A push to . writes nothing to GitHub and is not read.
  • How config is read: git's configuration files are read directly, with no --dry-run, because that reaches the network and the credential helper.

A push setting given by -c in the command is refused, not read. There is one shared PUSH_KEYS list, which adds remote.pushDefault and remote.*.mirror.

A git the command may redefine is refused when the command commits or pushes:

  • a function or alias named git, gh, command, builtin or an allowed wrapper (zsh's functions[]/aliases[], hash and enable count);
  • PATH set;
  • exec or builtin;
  • an unlisted wrapper, sudo among them;
  • a program in a variable the hook cannot read, given a write verb.

git counts only in command position, past the wrappers the shared reader resolves (tools/hooks/shell.py, reused from #142). So gitleaks git <path> is no longer refused as an unknown git command; it was refused on main before this.

Working rulings taken without the Maintainer, open to review: a PATH set in a commit or push command is refused, and so is a commit or push through an unlisted wrapper such as sudo.

Decision 0024 adds this to 0017. 0017's Status is unchanged, since check_decisions allows a Status to change only to "replaced by".

Verified on a plain git archive export with PATH=/usr/bin:/bin, at aa68d54:

  • make verify passes (516 tests).
  • python3 -m tools.mutants kills 436 of 436, and each identity and shell mutant fails its own test.
  • check_decisions is clean.

Reviewed by: a fresh-context Claude review and a security pass. All three false refusals they found are fixed:

  • gitleaks git <path>;
  • every bare git push refused because some other branch had a local upstream;
  • env -- git push.

AGY (Gemini 4 Argon) on f95da81, and CodeRabbit: ten findings, all real, fixed in 46116b5 and aa68d54. This was the one fix round for spellings.

  • False refusal: commit-message or PR-body prose mentioning PATH was refused. Only shell words are read now.
  • A missed wrapper argument: timeout -- 60 claude … read "60" as the program.
  • Variables: identity variables and PATH passed through a variable to env.
  • which redefined: which, type and whence count as redefinitions, and only a plain name inside $(which …) is rewritten.
  • Wrappers by path: a wrapper given by a path, such as ./nice, refuses.
  • Function names: quoted or escaped names, and zsh's several names after function, are caught.
  • Empty config: an empty push.default asks.
  • Reserved words: the shell-source check reads past time, if and !.
  • Record wording: 0024 no longer recounts review history.

Five spellings that no session would write by mistake are filed as #148. The Stop check and the rulesets are the boundary (#23's ruling).

Also: a pre-check keeps the protected hook inside its 5-second budget on a 200 KB command under load.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Push checks now account for configured remotes, default destinations, refspecs, and mirror settings when no destination is specified.
    • The identity check refuses pushes when relevant Git settings cannot be read or resolved, and flags commands that may conceal or redefine Git or GitHub CLI.
    • Pushes to local repositories are handled without GitHub-account or commit checks. Protected-branch checks handle ambiguous push destinations conservatively.
    • Identity checks now catch additional ways commands can change Git identity or configuration, including through expanded environment values.
  • Documentation
    • Updated guidance on hook command parsing and Git push configuration.