feat(rule): resolve a variable executable assigned in the same input - #53
Open
ronheichman wants to merge 4 commits into
Open
feat(rule): resolve a variable executable assigned in the same input#53ronheichman wants to merge 4 commits into
ronheichman wants to merge 4 commits into
Conversation
ronheichman
force-pushed
the
resolve-variable-executable
branch
2 times, most recently
from
September 8, 2026 14:46
bea5a06 to
e0e5a0e
Compare
A POSIX command whose first word is a variable is projected with the
variable's value as its executable when the same input assigned that
variable exactly once, by a plain top-level NAME=value statement with a
static one-word value, before the use, and the input writes variables no
other way. GH=/usr/bin/gh; $GH repo fork x then projects as a static
gh command, so a name-based enforce rule denies it. Every other run-time
first word keeps the current behavior: dropped with the diagnostic
"dynamic command executable".
The resolver walks the parsed input twice: it counts plain assignments
and loop bindings per variable, records the single top-level static
assignment per name, and gives up on the whole input when it sees a
write the count cannot follow: arithmetic, coproc, a {name}> redirect, a
zsh glob qualifier, a declaration with a non-option or rebinding
operand, a write to IFS, PS4, BASH_ENV, ENV, ZDOTDIR, or HOME,
printf -v, set -A, set -k, set -o keyword, shopt -o keyword, wait -p,
test -v on an array element, the builtins that read or evaluate into
variables (also behind command, builtin, noglob, nocorrect), or a
command whose first word is neither static nor resolved. Names the
shell manages, and values with whitespace, wildcards, ~, =, or $'...'
quoting, never resolve. A script passed to an interpreter resolves only
when the outer input resolves, the call sets no environment word, its
option words, script, and redirects are readable as written, and no
option selects keyword mode. The resolved word is substituted as a
literal into a shallow copy of the call before projection, so
projection, wrapper unwrapping, interpreter detection, and the compound
enforcement candidates see one static program.
Verified with 33 resolvable and 144 unresolvable forms, 89 source
mutations that each fail at least one test, sixteen independent review
passes, a differential replay of 9,574 inputs against the previous
build, race, shuffle, fuzz, the repository lint set, and a replay of
7,312 real commands with no finding or deny change against the base.
Built with Claude Code
ronheichman
force-pushed
the
resolve-variable-executable
branch
from
September 8, 2026 17:28
e0e5a0e to
1bb0f23
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A POSIX command whose first word is a variable now projects with the variable's value as its executable when the same input assigns that variable exactly once by a plain top-level
NAME=valuewith a static one-word value before the use and writes variables no other way, with every other case unchanged and every disabling condition listed indocs/rules.md.Why
GH=/usr/bin/gh; $GH repo fork xwas dropped as a dynamic executable and passed a name-based enforce rule that deniesgh repo fork x, and this closes that gap without emulating shell state (stacked on #46).How we verified it
33 resolvable and 144 unresolvable test forms, 89 source mutations each killed by a test, sixteen adversarial review passes, a differential replay of 9,574 inputs against the previous build, race, shuffle, fuzz, the repository lint set, and a replay of 7,312 real agent commands with no finding or deny change against the #46 base.
Built with Claude Code