Skip to content

fix: match commands quote-aware in block-cli-workarounds - #172

Open
ryzizub wants to merge 9 commits into
mainfrom
session/wary-finch-555u
Open

ryzizub wants to merge 9 commits into
mainfrom
session/wary-finch-555u

Conversation

@ryzizub

@ryzizub ryzizub commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #147.

The hook split on [;&|]+ with no quote awareness, so grep "a|flutter test|b" was denied and fvm flutter test was not.

What it does now

  1. Quoted text is data — unless eval, sh -c or $( ) runs it.
  2. Every adjacent word pair is checked against the basename, so wrappers need no list.

73 lines of code, no lexer.

Before / after

Command Before After
grep -nE "very_good|flutter test|foo" CLAUDE.md denied allowed
fvm flutter test, melos exec -- flutter test, sudo -u ci flutter test allowed denied
if true; then flutter test; fi, /usr/local/bin/flutter test, OUT="$(flutter test)" allowed denied
eval "flutter test", bash -c 'flutter test' allowed denied

Pinned gaps

  • Unquoted echo flutter test is denied. Quote it.
  • "flutter" test and F=flutter; $F test pass.

Also

🤖 Generated with Claude Code

@ryzizub
ryzizub requested a review from a team as a code owner October 5, 2026 13:22
@ryzizub
ryzizub marked this pull request as draft October 5, 2026 13:22
@ryzizub
ryzizub force-pushed the session/wary-finch-555u branch from 578851f to 5c5786c Compare October 5, 2026 13:26
ryzizub and others added 2 commits October 5, 2026 16:14
The matcher split on shell operators with no notion of quoting or command
position, so it denied read-only commands whose quoted arguments contained the
governed strings, and missed real invocations behind any prefix.

Two awk passes replace it. Pass 1 makes quoting inert, treating $( ) and
backticks as command positions even inside double quotes and ending a line at
an unquoted #. Pass 2 tests every adjacent token pair against the basename,
which drops the wrapper list and covers melos, timeout, sudo, xargs, nice,
shell keywords, brace groups and path-qualified binaries in one rule.

Closes #147

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read the whole command as one awk record, which removes the accumulator, the
END flush and all cross-record quote-state carrying, and makes backslash line
continuation fall out of the escape rule instead of needing its own branch. A
kind[] lookup replaces the five-branch conditional, and the two awk programs
are named so the pipeline reads in one line.

Behavior is unchanged: the existing 131 cases pass untouched. Two cases cover
a comment followed by a blocked command on a later line, which one record made
load-bearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ryzizub
ryzizub force-pushed the session/wary-finch-555u branch from 01bbcad to 89e7e17 Compare October 5, 2026 14:14
ryzizub and others added 6 commits October 5, 2026 22:29
The matcher emitted one of five tokens that the caller translated straight back
into a message, so the tokens existed only to be undone. The lookup table now
holds the redirect itself and the five-branch case statement is gone.

Reading each awk program from a quoted heredoc rather than a single-quoted
string also retires the -v SQ/-v DQ workaround, since the program can now
contain quote characters directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The character-level lexer existed to handle forms the agent never writes:
quoting the command name, a backslash-continued line, a # comment inside a
tool call. Three regex substitutions collapse quoted spans instead, and a
second scan with the quotes deleted runs only when something executes the
string -- eval, sh -c, or $( ) and backticks inside double quotes.

That catches eval "flutter test" and bash -c '...', which the lexer did not,
and drops "flutter" test and line continuation, which nobody types. The
non-goal tests flip accordingly; everything else passes untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scan() turns operators into a ";" word and walks one flat word list instead
of a nested fragment loop. The main block names its three views of the
command up front, so the decision reads as three gated scans. Comments say
what, not why.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The README row and CLAUDE.md bullet each said the same thing three ways. The file header now points at the hint table instead of duplicating its contents, and notes why the program is read with read rather than a command substitution.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The trigger words were assembled from fragments so the pair never appeared in this file, protecting shell edits of it from the hook under test. Single words are harmless and only a heredoc write was ever at risk, so the file now reads plainly and a header note covers that one case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ryzizub
ryzizub marked this pull request as ready for review October 5, 2026 21:22
@unicoderbot

unicoderbot Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Heads up — I'm auto-updating this PR by merging the latest main into this branch. No action needed; I'll comment again only if this update hits a conflict or an unexpected error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: block-cli-workarounds denies commands with a quoted regex alternation

2 participants