Both release guards recognise a command only when its name is written bare. An escaped, quoted or path-qualified command name runs the same program and is not checked.
Measured at c4427df (the head of #155) with the guards fed the PreToolUse JSON payload:
guard-gh-release.py
gh api repos/o/r/releases/1 -X DELETE -> 2 (blocked)
\gh api repos/o/r/releases/1 -X DELETE -> 0
"gh" api repos/o/r/releases/1 -X DELETE -> 0
/usr/bin/gh api repos/o/r/releases/1 -X DELETE -> 0
/usr/bin/gh release create v1 -> 0
guard-lightweight-tag.py
git tag v1.2.3 -> 2 (blocked)
\git tag v1.2.3 -> 0
"git" tag v1.2.3 -> 0
/usr/bin/git tag v1.2.3 -> 0
The fourth review of #155 found these at both 52a6996 and 59b9e75 as well, so the gap predates that PR.
Cause
GH_RELEASE_RE, GH_API_RE and the tag guard's patterns match INVOCATION_PREFIX followed by the literal word gh or git. A leading \, surrounding quotes or a directory prefix changes the text of the word without changing what the shell runs.
What closing it needs
- Normalise the command word of each invocation before matching: drop a leading
\, remove surrounding quotes, and reduce a path to its basename. This belongs in _invocations.py, so both guards get it.
- Tests for each of the forms above, in both invocation suites.
Related: #157 covers commands wrapped in bash -c or fed to a shell through a heredoc.
Assisted by claude-code:claude-opus-5-5 — Session
Both release guards recognise a command only when its name is written bare. An escaped, quoted or path-qualified command name runs the same program and is not checked.
Measured at
c4427df(the head of #155) with the guards fed the PreToolUse JSON payload:The fourth review of #155 found these at both
52a6996and59b9e75as well, so the gap predates that PR.Cause
GH_RELEASE_RE,GH_API_REand the tag guard's patterns matchINVOCATION_PREFIXfollowed by the literal wordghorgit. A leading\, surrounding quotes or a directory prefix changes the text of the word without changing what the shell runs.What closing it needs
\, remove surrounding quotes, and reduce a path to its basename. This belongs in_invocations.py, so both guards get it.Related: #157 covers commands wrapped in
bash -cor fed to a shell through a heredoc.Assisted by claude-code:claude-opus-5-5 — Session