Identity hook: a write into git's directory, an alias that runs git, and xargs git are refused

Merged
DO
2d ago
+684-19

Closes #148.

These spellings were left after the push-reading story (#111, PR #149). The rulings are on #148, and decision 0030 records them.

What changes

  • A write into git's directory in a command that commits or pushes is refused (git_directory_written).
    • What counts as a write: only a redirect's target, or the destination of a known writer: cp, mv, ln, install, tee, rsync, tar, dd, and sed/perl -i.
    • Paths: resolved through cd, .., links and case. A loop variable is read as each word of its list. A glob or brace that can expand to .git counts; * alone doesn't, as in the shell.
    • Not writes: find -not -path '*/.git/*', fd -E .git, tree -I .git, rsync --exclude .git, sed -n/awk/stat/du reading .git, and git's own words such as git commit -F .git/MERGE_MSG.
  • An alias whose value runs git or gh is refused, with quotes removed, both as alias and as zsh's aliases[g]. So alias g=git is refused on its own. An alias value the hook cannot read is refused too.
  • xargs or parallel giving git no verb is git_command_unknown. xargs git add passes.
  • Loops and case: a for/select word list and every case arm's pattern are data. This fixes a false refusal of a read-only loop over file names that contain "git" and "commit".
  • A gh alias is left to the Stop check and the rulesets: it doesn't change which account writes. That is a working ruling.

Verified:

  • make verify passes (531 tests) on a plain git archive export with PATH=/usr/bin:/bin. The record was renumbered there only, for the gap.
  • Each of the 25 mutants was killed by its own test, checked one at a time by hand. CI's shards run the set.
  • check_decisions is clean.

Reviewed by: a fresh-context Claude review, a security pass, and AGY (Gemini 4 Argon) on 698564a. Everything is fixed in 6de5f42; this was the story's one fix round for spellings.

  • False refusals fixed: find/fd/tree excludes, git commit -F .git/MERGE_MSG, readers of .git, and case arms after the first line.
  • Security gaps closed: a quote-split alias, a for loop binding a .git path, a glob into .git, and two branches with no mutant.
  • AGY's zsh table-alias case was already refused, and is now tested.

Left, to be filed:

  • cp x .git/hooks/pre-push on its own is still refused, as on main, because "pre-push" in the path matches.
  • An unlisted writer such as python3 -c writing into .git passes.

Order: merges after #16 (decision 0029).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Safety
    • Improved safeguards against commands that could alter Git metadata before commit or push operations.
    • Added checks for Git and GitHub CLI commands invoked through aliases or input-fed commands.
    • Shell loops and case statements are now interpreted more accurately, distinguishing commands that run from patterns and other data.
  • Documentation
    • Added guidance describing these command-handling rules and their boundaries.