Summary
block-hook-bypass.sh's _py_write indicator includes a bare open[[:space:]]*\(, which matches any open( — including a read. Since open() defaults to mode 'r', a python3 -c that only reads a file is blocked with a message asserting it writes.
Filing as a precision question, not a clear-cut bug: this lane documents an "ACCEPTED OVER-BLOCK" posture (the fail-closed choice approved for the PowerShell branch, block-hook-bypass.sh:468-475). But that documented acceptance covers prose/comment/quoted-string mentions of a write. A genuine read-only open() is a different category, and it is not called out as accepted anywhere I could find.
Related but distinct: #1178 (CLOSED) fixed the path( sub-indicator's unanchored match. This is the sibling open( sub-indicator, and it is a mode gap rather than an anchoring gap — so #1178's fix does not cover it and this is not a regression of it.
Reproduction (live, not constructed)
Hit incidentally while reading a JSON settings file during unrelated work:
python3 -c "
import json,os
p=os.path.expanduser('~/.claude/settings.json')
d=json.load(open(p,encoding='utf-8'))
print(json.dumps(d.get('statusLine'),indent=2))
"
Blocked with:
PreToolUse:Bash hook error: ["${CLAUDE_PLUGIN_ROOT}"/hooks/block-hook-bypass.sh]:
BLOCKED: python3 -c file write bypasses Write/Edit hooks
Use the Write or Edit tool instead of a shell file-write workaround.
There is no write in that command. The only open() is a read, and the sole side effect is print.
Mechanism
block-hook-bypass.sh:313:
_py_write='open[[:space:]]*\(|\.write[[:space:]]*\(|(^|[^[:alnum:]_])pathlib|(^|[^[:alnum:]_])path[[:space:]]*\('
The first alternative matches open( with no regard for mode. The call site at :505-508 gates only on "is this a python3 -c invocation?" plus that indicator:
if [[ "$EXEC_LC" =~ (^|[[:space:];|&()/\\]+)python3(\.exe)?[[:space:]]+-c ]] &&
[[ "$COMMAND_LC" =~ $_py_write ]]; then
block_bypass "python-write" "python3 -c file write bypasses Write/Edit hooks"
fi
Note the asymmetry with the PowerShell branch at :477, which requires a second confirming predicate (ps::might_write_via_python3) before blocking. The python3 -c site has no equivalent corroboration step.
Why it is worth precision work
json.load(open(p)) / open(p).read() is the most common way to read a config or JSON file in a one-liner, so this fires on routine read-only inspection rather than on anything resembling a bypass attempt. Two costs:
- The diagnostic is false. It states the command writes and instructs the operator to "use the Write or Edit tool instead" — advice that cannot apply to a read. An operator who trusts the message is misled about what their own command did.
- It trains around the guard. The natural response is to reach for another reader (
jq, cat, Get-Content) until one is not blocked — which is exactly the "routine alternative after a denial" behavior pattern that makes over-blocking counterproductive.
Possible directions (not a prescription)
- Require a write-mode signal near the
open( — e.g. a second argument containing w/a/x/+, or mode=. A bare open(x) and open(x, 'r') are unambiguously reads.
- Or mirror the PowerShell branch: keep
open( as a cheap indicator but require a corroborating predicate before blocking, so the indicator alone cannot deny.
- Or, if the over-block is genuinely intended here, say so in the block message — "read-only
open() is also blocked; use Read/Grep" — so the diagnostic stops making a false factual claim.
Whichever way it goes, the message text should not assert a write that did not happen.
Environment
Windows 11 / Git Bash, CLI 2.1.220, guardrails as installed from the melodic-software marketplace at the time of the block. Reproduced once, incidentally; I have not swept for other read-only idioms that trip the same indicator (pathlib and the .write( alternative were not exercised).
Summary
block-hook-bypass.sh's_py_writeindicator includes a bareopen[[:space:]]*\(, which matches anyopen(— including a read. Sinceopen()defaults to mode'r', apython3 -cthat only reads a file is blocked with a message asserting it writes.Filing as a precision question, not a clear-cut bug: this lane documents an "ACCEPTED OVER-BLOCK" posture (the fail-closed choice approved for the PowerShell branch,
block-hook-bypass.sh:468-475). But that documented acceptance covers prose/comment/quoted-string mentions of a write. A genuine read-onlyopen()is a different category, and it is not called out as accepted anywhere I could find.Related but distinct: #1178 (CLOSED) fixed the
path(sub-indicator's unanchored match. This is the siblingopen(sub-indicator, and it is a mode gap rather than an anchoring gap — so #1178's fix does not cover it and this is not a regression of it.Reproduction (live, not constructed)
Hit incidentally while reading a JSON settings file during unrelated work:
Blocked with:
There is no write in that command. The only
open()is a read, and the sole side effect isprint.Mechanism
block-hook-bypass.sh:313:The first alternative matches
open(with no regard for mode. The call site at:505-508gates only on "is this apython3 -cinvocation?" plus that indicator:Note the asymmetry with the PowerShell branch at
:477, which requires a second confirming predicate (ps::might_write_via_python3) before blocking. Thepython3 -csite has no equivalent corroboration step.Why it is worth precision work
json.load(open(p))/open(p).read()is the most common way to read a config or JSON file in a one-liner, so this fires on routine read-only inspection rather than on anything resembling a bypass attempt. Two costs:jq,cat,Get-Content) until one is not blocked — which is exactly the "routine alternative after a denial" behavior pattern that makes over-blocking counterproductive.Possible directions (not a prescription)
open(— e.g. a second argument containingw/a/x/+, ormode=. A bareopen(x)andopen(x, 'r')are unambiguously reads.open(as a cheap indicator but require a corroborating predicate before blocking, so the indicator alone cannot deny.open()is also blocked; use Read/Grep" — so the diagnostic stops making a false factual claim.Whichever way it goes, the message text should not assert a write that did not happen.
Environment
Windows 11 / Git Bash, CLI 2.1.220,
guardrailsas installed from the melodic-software marketplace at the time of the block. Reproduced once, incidentally; I have not swept for other read-only idioms that trip the same indicator (pathliband the.write(alternative were not exercised).