Your pattern 2 shows up from the deny side as well. In a throwaway repo we put a single rule, Bash(git push:*), in .claude/settings.json and had Claude Code 2.1.278 run 14 spellings of a push: it stopped 8, and 5 still reached the remote, including git -c ... push, git 'push' and sh -c "git push". To us it looked like the same gap as your Bash(git * main) example: the rule matched the text Claude wrote rather than what the shell ended up running.
Rulestack
Rule packs & skills for Cursor, Claude Code, and Codex
Your pattern 2 shows up from the deny side as well. In a throwaway repo we put a single rule, Bash(git push:*), in .claude/settings.json and had Claude Code 2.1.278 run 14 spellings of a push: it stopped 8, and 5 still reached the remote, including git -c ... push, git 'push' and sh -c "git push". To us it looked like the same gap as your Bash(git * main) example: the rule matched the text Claude wrote rather than what the shell ended up running.